Commit 98e50362 authored by jackfrued's avatar jackfrued

更新了部分文档

parent 6d7fb903
......@@ -2,84 +2,91 @@
### Web应用
问题1:描述一个Web应用的工作流程。
![](./res/web-application.png)
问题1:描述一个Web应用的工作流程。(如上图所示)
问题2:描述项目的物理架构。(上图中补充负载均衡(反向代理)服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等,而且每个节点都有可能是多节点构成的集群,如下图所示)
问题2:描述项目的物理架构。(上图中补充反向代理服务器、负载均衡服务器、数据库服务器、文件服务器、缓存服务器、防火墙等,每个节点都有可能是多节点构成的集群)
![](./res/05.django_massive_cluster.png)
问题3:描述Django项目的工作流程。(如下图所示)
![](./res/django-flowchart.png)
![](./res/django_request_response_cycle.png)
### MVC架构模式
![](./res/mvc.png)
问题1:为什么要使用MVC架构模式?(模型和视图解耦合)
问题2:MVC架构中每个部分的作用?(如图所示)
问题2:MVC架构中每个部分的作用?(如图所示)
### HTTP请求和响应
![](./res/mvc.png)
#### HTTP请求
### HTTP请求和响应
HTTP请求 = 请求行+请求头+空行+[消息体]
#### HTTP请求 = 请求行+请求头+空行+[消息体]
![](./res/http-request.png)
HTTP响应 = 响应行+响应头+空行+消息体
#### HTTP响应 = 响应行+响应头+空行+消息体
![](./res/http-response.png)
1. `HTTPRequest`对象的属性和方法:
- `method`
- `path` / `get_full_path()`
- `scheme` / `is_secure()` / `get_host()` / `get_port()`
- `META` / `COOKIES`
- `GET` / `POST` / `FILES`
- `get_signed_cookie()`
- `is_ajax()`
- `body` / `content_type` / `encoding`
- `method` - 获取请求方法
- `path` / `get_full_path()` - 获取请求路径/带查询字符串的路径
- `scheme` / `is_secure()` / `get_host()` / `get_port()` - 获取请求的协议/主机/端口
- `META` / `COOKIES` - 获取请求头/Cookie信息
- `GET` / `POST` / `FILES` - 获取GET或POST请求参数/上传的文件
- `get_signed_cookie()` - 获取带签名的Cookie
- `is_ajax()` - 是不是Ajax异步请求
- `body` / `content_type` / `encoding` - 获取请求的消息体(bytes流)/MIME类型/编码
2. 中间件添加的属性:
- `session` / `user` / `site`
3. `HttpResponse`对象的属性和方法:
- `set_cookie()` / `set_signed_cookie()` / `delete_cookie()`
- `__setitem__` / `__getitem__` / `__delitem__`
- `charset` / `content` / `status_code`
4. `JsonResponse``HttpResponse`的子类型)对象
```Python
class HouseJsonEncoder(JsonEncoder):
def default(self, o):
# 定义如何将对象转成dict类型并返回这个字典
pass
```
- `set_cookie()` / `set_signed_cookie()` / `delete_cookie()` - 添加/删除Cookie
- `__setitem__` / `__getitem__` / `__delitem__` - 添加/获取/删除响应头
- `charset` / `content` / `status_code` - 响应的字符集/消息体(bytes流)/状态码
- 1xx:请求已经收到,继续处理
- 2xx(成功):请求已经成功收到、理解和接收。
- 3xx(重定向):为完成请求要继续执行后续的操作。
- 4xx(客户端错误):请求不正确或不能够被受理。
- 5xx(服务器错误):服务器处理请求失败。
4. `JsonResponse``HttpResponse`的子类型)对象
```Python
>>> from django.http import JsonResponse
>>> from django.http import HttpResponse, JsonResponse
>>>
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
>>>
>>> response = JsonResponse([1, 2, 3], safe=False)
>>> response = JsonResponse(house, encoder=HouseJsonEncoder)
>>> response = HttpResponse('')
>>> response.content
>>>
>>> response = HttpResponse(b'...')
>>> response['cotent-type'] = 'application/pdf';
>>> response['content-disposition'] = 'inline; filename="xyz.pdf"'
>>> response['content-disposition'] = 'attachment; filename="xyz.pdf"'
>>> response.set_signed_cookie('', '', salt='')
>>>
>>> response.set_signed_cookie('foo', 'bar', salt='')
>>> response.status_code = 200
```
### 数据模型(Model)
问题1:关系型数据库表的设计应该注意哪些问题?(范式理论)
问题1:关系型数据库表的设计应该注意哪些问题(范式理论和逆范式)?如何通过表来创建模型类(反向工程)?如何通过模型类来创建表(正向工程)?
问题2:关系型数据库中数据完整性指的是什么?(实体完整性/参照完整性/域完整性)
```Shell
python manage.py makemigrations <appname>
python manage.py migrate
python manage.py inspectdb > <appname>/models.py
```
问题2:关系型数据库中数据完整性指的是什么?什么时候需要牺牲数据完整性?(实体完整性/参照完整性/域完整性)
问题3:ORM是什么以及解决了什么问题?(对象模型-关系模型双向转换)
......@@ -114,7 +121,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
- `related_name`
```Python
class Dept(models.Model):
pass
......@@ -126,22 +132,31 @@ HTTP响应 = 响应行+响应头+空行+消息体
Dept.objects.get(no=10).emp_set.all()
Emp.objects.filter(dept__no=10)
```
> 说明:`related_name`设置为`'+'`,可以防止一对多外键关联从“一”的一方查询“多”的一方。
- 其他属性:
- `to_field` / `limit_choices_to` / `swappable`
3. `Model`的属性和方法
- `objects`/ `pk`
- `objects` / `pk`
- `save()` / `delete()`
- `from_db()` / `get_XXX_display()` / `clean()` / `full_clean()`
- `clean()` / `validate_unique()` / `full_clean()`
4. `QuerySet`的方法
- `get()` / `all()` / `values()`
> 说明:`values()`返回的`QuerySet`中不是模型对象而是字典
- `count()` / `order_by()` / `exists()` / `reverse()`
- `filter()` / `exclude()`
- `exact` / `iexact`:精确匹配/忽略大小写的精确匹配查询
- `contains` / `icontains` / `startswith / istartswith / endswith / iendswith`:基于`like`的模糊查询
......@@ -165,7 +180,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
- `Avg` / `Count` / `Sum` / `Max` / `Min`
```Python
>>> from django.db.models import Avg
>>> Emp.objects.aggregate(avg_sal=Avg('sal'))
(0.001) SELECT AVG(`TbEmp`.`sal`) AS `avg_sal` FROM `TbEmp`; args=()
......@@ -173,7 +187,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
```
```Python
>>> Emp.objects.values('dept').annotate(total=Count('dept'))
(0.001) SELECT `TbEmp`.`dno`, COUNT(`TbEmp`.`dno`) AS `total` FROM `TbEmp` GROUP BY `TbEmp`.`dno` ORDER BY NULL LIMIT 21; args=()
<QuerySet [{'dept': 10, 'total': 4}, {'dept': 20, 'total': 7}, {'dept': 30, 'total': 3}]
......@@ -181,10 +194,11 @@ HTTP响应 = 响应行+响应头+空行+消息体
- `first()` / `last()`
> 说明:调用`first()`方法相当于用`[0]`对`QuerySet`进行切片。
- `only()` / `defer()`
```Python
>>> Emp.objects.filter(pk=7800).only('name', 'sal')
(0.001) SELECT `TbEmp`.`empno`, `TbEmp`.`ename`, `TbEmp`.`sal` FROM `TbEmp` WHERE `TbEmp`.`empno` = 7800 LIMIT 21; args=(7800,)
<QuerySet [<Emp: Emp object (7800)>]>
......@@ -196,7 +210,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
- `create()` / `update()` / `raw()`
```Python
>>> Emp.objects.filter(dept__no=20).update(sal=F('sal') + 100)
(0.011) UPDATE `TbEmp` SET `sal` = (`TbEmp`.`sal` + 100) WHERE `TbEmp`.`dno` = 20; args=(100, 20)
>>>
......@@ -207,7 +220,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
5. `Q`对象和`F`对象
```Python
>>> from django.db.models import Q
>>> Emp.objects.filter(
... Q(name__startswith='张'),
......@@ -217,7 +229,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
```
```Python
reporter = Reporters.objects.filter(name='Tintin')
reporter.update(stories_filed=F('stories_filed') + 1)
```
......@@ -225,9 +236,9 @@ HTTP响应 = 响应行+响应头+空行+消息体
6. 原生SQL查询
```Python
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("UPDATE TbEmp SET sal=sal+10 WHERE dno=30")
cursor.execute("SELECT ename, job FROM TbEmp WHERE dno=10")
......@@ -237,7 +248,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
7. 模型管理器
```Python
class BookManager(models.Manager):
def title_count(self, keyword):
......@@ -250,22 +260,30 @@ HTTP响应 = 响应行+响应头+空行+消息体
1. 用户的每个操作对应一个视图函数。
2. 每个视图函数构成一个事务边界。
- 事务的概念。
2. [每个视图函数可以构成一个事务边界](https://docs.djangoproject.com/en/2.1/ref/settings/)。
- 事务的ACID特性。
- 事务隔离级别。
- 原子性(Atomicity):事务中各项的操作要么全做要么全不做;
- 一致性(Consistentcy):事务前后系统的状态是一致的;
- 隔离性(Isolation):并发执行的事务无法看到彼此的中间状态;
- 持久性(Duration):事务完成后所做的改动都会被持久化。
- 事务隔离级别 - 设置事务隔离级别是为了数据库底层依据事务隔离级别为数据加上适当的锁。如果需要保证数据的强一致性,那么关系型数据库仍然是唯一的也是最好的选择,因为关系型数据库可以通过锁机制来保护数据。事务隔离级别从低到高依次是:Read Uncommitted(读未提交)、Read Committed(读提交)、Repeatable Read(可重复读)、Serializable(串行化)。事务隔离级别越高,数据并发访问的问题越少,但是性能越差;事务隔离级别越低,数据并发访问的问题越多,但是性能越好。
Read Uncommitted < Read Committed < Repeatable Read < Serializable
- 数据并发访问会产生5种问题(请参考我的[《Java面试题全集(上)》](https://blog.csdn.net/jackfrued/article/details/44921941)第80题对该问题的讲解):
- 第1类丢失更新(A事务撤销覆盖B事务更新的数据)和第2类丢失更新(A事务提交覆盖B事务更新的数据)。
- 脏读(读脏数据):一个事务读取到其他尚未提交的事务的数据。
- 不可重复读: 一个事务在读取它的查询结果时,被另一个事务更新了它的查询记录导致无法读到数据。
- 幻读:一个事务在读取它的查询结果时,发现读到了被另一个事务提交的新数据。
```SQL
-- 设置全局默认的事务隔离级别
set global transaction isolation level repeatable read;
-- 设置当前会话的事务隔离级别
set session transaction isolation level read committed;
-- 查询当前会话的事务隔离级别
select @@tx_isolation;
```
......@@ -274,35 +292,30 @@ HTTP响应 = 响应行+响应头+空行+消息体
- 给每个请求绑定事务环境(反模式)。
```Python
ATOMIC_REQUESTS = True
```
- 使用事务装饰器。
- 使用事务装饰器(简单易用)
```Python
@transaction.non_atomic_requests
@transaction.atomic
```
- 使用上下文语法。
- 使用上下文语法(事务控制的范围更加精准)
```Python
with transaction.atomic():
pass
```
- 关闭自动提交。
- 关闭自动提交使用手动提交
```Python
AUTOCOMMIT = False
```
```Python
transaction.commit()
transaction.rollback()
```
......@@ -312,7 +325,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
1. 可以让部分URL只在调试模式下生效。
```Python
from django.conf import settings
urlpatterns = [
......@@ -326,7 +338,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
2. 可以使用命名捕获组捕获路径参数。
```Python
url(r'api/code/(?P<mobile>1[3-9]\d{9})'),
path('api/code/<str:mobile>'),
```
......@@ -342,7 +353,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
7. 可以用`reverse`函数实现URL的逆向解析(从名字解析出URL),在模板中也可以用`{% url %}`实现同样的操作。
```Python
path('', views.index, name='index')
return redirect(reverse('index'))
......@@ -357,7 +367,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
1. 模板的配置和渲染函数。
```Python
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
......@@ -376,8 +385,7 @@ HTTP响应 = 响应行+响应头+空行+消息体
```
```Python
resp = render(request, 'foo.html', {'foo': 'bar'})
resp = render(request, 'index.html', {'foo': ...})
```
2. 模板遇到变量名的查找顺序。
......@@ -411,7 +419,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
- 文件系统加载器
```Python
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
......@@ -421,7 +428,6 @@ HTTP响应 = 响应行+响应头+空行+消息体
- 应用目录加载器
```Python
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
......@@ -438,7 +444,7 @@ HTTP响应 = 响应行+响应头+空行+消息体
#### 其他视图
1. MIME类型。
1. MIME(多用途Internet邮件扩展)类型 - 告知浏览器传输的数据类型。
| Content-Type | 说明 |
| ---------------- | ------------------------------------------------------------ |
......@@ -454,55 +460,52 @@ HTTP响应 = 响应行+响应头+空行+消息体
| video/mp4 | [MP4](https://zh.wikipedia.org/wiki/MP4)视频文件 |
| video/quicktime | [QuickTime](https://zh.wikipedia.org/wiki/QuickTime)视频文件 |
2. 如何处置生成的内容。
2. 如何处置生成的内容(inline / attachment)
```Python
response['content-type'] = 'application/pdf'
response['content-disposition'] = 'attachment; filename="xyz.pdf"'
>>> from urllib.parse import quote
>>>
>>> response['content-type'] = 'application/pdf'
>>> filename = quote('Python语言规范.pdf')
>>> filename
'Python%E8%AF%AD%E8%A8%80%E8%A7%84%E8%8C%83.pdf'
>>> response['content-disposition'] = f'attachment; filename="{filename}"'
```
> 提醒:URL以及请求和响应中的中文都应该处理成[百分号编码](https://zh.wikipedia.org/zh-hans/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81)。
> 提醒:URL以及请求和响应中的中文都应该处理成[百分号编码](https://zh.wikipedia.org/zh-hans/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81)。
3. 生成CSV / Excel / PDF。
3. 生成CSV / Excel / PDF / 统计报表
- 向浏览器传输二进制数据。
```Python
buffer = ByteIO()
resp = HttpResponse(content_type='...')
resp['Content-Disposition'] = 'attachment;filename="..."'
resp['Content-Disposition'] = 'attachment; filename="..."'
resp.write(buffer.getvalue())
```
- 大文件的流式处理:`StreamingHttpResponse`。
```Python
class EchoBuffer(object):
def write(self, value):
return value
def some_streaming_csv_view(request):
rows = (["Row {}".format(idx), str(idx)] for idx in range(65536))
writer = csv.writer(EchoBuffer())
resp = StreamingHttpResponse((writer.writerow(row) for row in rows),
content_type="text/csv")
resp['Content-Disposition'] = 'attachment; filename="data.csv"'
def download_file(request):
file_stream = open('...', 'rb')
# 如果文件的二进制数据较大则最好用迭代器进行处理避免过多的占用服务器内存
file_iter = iter(lambda: file_stream.read(4096), b'')
resp = StreamingHttpResponse(file_iter)
# 中文文件名要处理成百分号编码
filename = quote('...', 'utf-8')
resp['Content-Type'] = '...'
resp['Content-Disposition'] = f'attachment; filename="{filename}"'
return resp
```
- 生成PDF:需要安装`reportlab`。
> 说明:如果需要生成PDF文件,可以需要安装`reportlab`;生成Excel可以使用`openpyxl`或`xlrd`。
- 生成Excel:需要安装`openpyxl`
- [ECharts](http://echarts.baidu.com/)或[Chart.js](https://www.chartjs.org/)
4. 统计图表。
- 思路:后端只提供JSON格式的数据,前端JavaScript渲染生成图表。
- [ECharts](http://echarts.baidu.com/)或[Chart.js](https://www.chartjs.org/)。
- 思路:后端只提供JSON格式的数据,前端JavaScript渲染生成图表。
### 中间件
......@@ -510,14 +513,11 @@ HTTP响应 = 响应行+响应头+空行+消息体
问题2:中间件有哪些不同的实现方式?(参考下面的代码)
问题4:描述Django内置的中间件及其执行顺序。
推荐阅读:[Django官方文档 - 中间件 - 中间件顺序](https://docs.djangoproject.com/zh-hans/2.0/ref/middleware/#middleware-ordering)。
问题3:描述Django内置的中间件及其执行顺序。(推荐阅读:[Django官方文档 - 中间件 - 中间件顺序](https://docs.djangoproject.com/zh-hans/2.0/ref/middleware/#middleware-ordering))
#### 激活中间件
```Python
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
......@@ -534,7 +534,6 @@ MIDDLEWARE = [
```Python
def simple_middleware(get_response):
def middleware(request):
......@@ -547,7 +546,6 @@ def simple_middleware(get_response):
```
```Python
class MyMiddleware(object):
def __init__(self, get_response):
......@@ -565,7 +563,6 @@ class MyMiddleware(object):
```
```Python
class MyMiddleware(object):
def __init__(self):
......@@ -590,20 +587,26 @@ class MyMiddleware(object):
#### 内置中间件
1. CommonMiddleware
- DISALLOWED_USER_AGENTS
- APPEND_SLASH
- USE_ETAG
2. SecurityMiddleware
- SECURE_HSTS_SECONDS
- SECURE_HSTS_INCLUDE_SUBDOMAINS
- SECURE_CONTENT_TYPE_NOSNIFF
- SECURE_BROWSER_XSS_FILTER
- SECURE_SSL_REDIRECT
- SECURE_REDIRECT_EXEMPT
3. SessionMiddleware
4. CsrfViewMiddleware - 防范跨栈身份伪造。
5. XFrameOptionsMiddleware - 防范点击劫持攻击
1. CommonMiddleware - 基础设置中间件
- DISALLOWED_USER_AGENTS - 不被允许的用户代理(浏览器)
- APPEND_SLASH - 是否追加`/`
- USE_ETAG - 浏览器缓存相关
2. SecurityMiddleware - 安全相关中间件
- SECURE_HSTS_SECONDS - 强制使用HTTPS的时间
- SECURE_HSTS_INCLUDE_SUBDOMAINS - HTTPS是否覆盖子域名
- SECURE_CONTENT_TYPE_NOSNIFF - 是否允许浏览器推断内容类型
- SECURE_BROWSER_XSS_FILTER - 是否启用跨站脚本攻击过滤器
- SECURE_SSL_REDIRECT - 是否重定向到HTTPS连接
- SECURE_REDIRECT_EXEMPT - 免除重定向到HTTPS
3. SessionMiddleware - 会话中间件
4. CsrfViewMiddleware - 防范跨站身份伪造中间件
5. XFrameOptionsMiddleware - 防范点击劫持攻击中间件
![](./res/click-jacking.png)
![](./res/builtin-middlewares.png)
......@@ -627,10 +630,24 @@ class MyMiddleware(object):
问题1:使用Cookie能解决什么问题?(用户跟踪,解决HTTP协议无状态问题)
1. URL重写
```
http://www.abc.com/path/resource?foo=bar
```
2. 隐藏域(隐式表单域)
```HTML
<form action="" method="post">
<input type="hidden" name="foo" value="bar">
</form>
```
3. Cookie
问题2:Cookie和Session之间关系是什么?(Session的标识会通过Cookie记录
问题2:Cookie和Session之间关系是什么?(Session的标识通过Cookie保存和传输
#### Session的配置
......@@ -641,7 +658,6 @@ class MyMiddleware(object):
- 基于数据库(默认方式)
```Python
INSTALLED_APPS = [
'django.contrib.sessions',
]
......@@ -650,9 +666,8 @@ class MyMiddleware(object):
- 基于缓存(推荐使用)
```Python
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
SESSION_CACHE_ALIAS = 'session'
```
- 基于文件(基本不考虑)
......@@ -660,14 +675,12 @@ class MyMiddleware(object):
- 基于Cookie(不靠谱)
```Python
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
```
3. Cookie相关的配置。
```Python
SESSION_COOKIE_NAME = 'djang_session_id'
SESSION_COOKIE_AGE = 1209600
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
......@@ -679,20 +692,19 @@ class MyMiddleware(object):
- `session_key` / `session_data` / `expire_date`
- `__getitem__` / `__setitem__` / `__delitem__` / `__contains__`
- `set_expiry()` / `get_expiry_age()` / `get_expiry_date()`
- `flush()`
- `set_test_cookie()` / `test_cookie_worked()` / `delete_test_cookie()`
- `set_expiry()` / `get_expiry_age()` / `get_expiry_date()` - 设置/获取会话超期时间
- `flush()` - 销毁会话
- `set_test_cookie()` / `test_cookie_worked()` / `delete_test_cookie()` - 测试浏览器是否支持Cookie(提示用户如果浏览器禁用Cookie可能会影响网站的使用)
5. session的序列化。
```Python
SESSION_SERIALIZER = django.contrib.sessions.serializers.JSONSerializer
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'
```
- JSONSerializer(默认)- 如果想将自定义的对象放到session中,会遇到“Object of type 'XXX' is not JSON serializable”的问题。
- PickleSerializer(1.6以前的默认,但是因为安全问题不推荐使用,但是只要不去反序列化用户构造的恶意的Payload就行了。关于这种方式的安全漏洞,请参考《[Python Pickle的任意代码执行漏洞实践和Payload构造》](http://www.polaris-lab.com/index.php/archives/178/)。)
- JSONSerializer(1.6及以后默认)- 如果想将自定义的对象放到session中,会遇到“Object of type 'XXX' is not JSON serializable”的问题。
- PickleSerializer(1.6以前的默认)- 因为安全问题不推荐使用,但是只要不去反序列化用户构造的恶意的Payload其实也没有什么风险。关于这种方式的安全漏洞,可以参考《[Python Pickle的任意代码执行漏洞实践和Payload构造》](http://www.polaris-lab.com/index.php/archives/178/)一文或《软件架构-Python语言实现》上关于这个问题的讲解。
- 说明:如果使用了django_redis整合Redis作为session的存储引擎,那么由于django_redis又封装了一个PickleSerializer来提供序列化,所以不会存在上述的问题,且Redis中保存的value是pickle序列化之后的结果。
### 缓存
......@@ -701,25 +713,66 @@ class MyMiddleware(object):
```Python
CACHES = {
# 默认缓存
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://120.77.222.217:6379/0',
'redis://120.77.222.217:6380/0',
'redis://120.77.222.217:6381/0',
# 'redis://120.77.222.217:6382/0',
'redis://1.2.3.4:6379/0',
],
'KEY_PREFIX': 'fang',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 1000,
},
'PASSWORD': '1qaz2wsx',
}
},
# 页面缓存
'page': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://1.2.3.4:6379/1',
],
# 'KEY_PREFIX': 'fang',
# 'TIMEOUT': 120,
'KEY_PREFIX': 'fang:page',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 100,
'max_connections': 500,
},
'PASSWORD': '1qaz2wsx',
}
},
# 会话缓存
'session': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://1.2.3.4:6379/2',
],
'KEY_PREFIX': 'fang:session',
'TIMEOUT': 1209600,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 2000,
},
'PASSWORD': '1qaz2wsx',
}
},
# 验证码缓存
'code': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://1.2.3.4:6379/3',
],
'KEY_PREFIX': 'fang:code:tel',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'CONNECTION_POOL_KWARGS': {
'max_connections': 500,
},
'PASSWORD': '1qaz2wsx',
# 'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor'
}
},
}
......@@ -727,7 +780,6 @@ CACHES = {
#### 全站缓存
```Python
MIDDLEWARE_CLASSES = [
'django.middleware.cache.UpdateCacheMiddleware',
...
......@@ -743,17 +795,17 @@ CACHE_MIDDLEWARE_KEY_PREFIX = 'djang:cache'
#### 视图层缓存
```Python
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_cookie
@cache_page(60 * 15)
@cache_page(timeout=60 * 15, cache='page')
@vary_on_cookie
def my_view(request):
pass
```
```Python
from django.views.decorators.cache import cache_page
urlpatterns = [
......@@ -770,35 +822,45 @@ urlpatterns = [
2. 使用底层API访问缓存。
```Python
>>> from django.core.cache import cache
>>>
>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
>>> cache.clear()
```
```Python
>>> from django.core.cache import caches
>>> cache1 = caches['myalias']
>>> cache2 = caches['myalias']
>>> cache1 = caches['page']
>>> cache2 = caches['page']
>>> cache1 is cache2
True
>>> cache3 = caches['session']
>>> cache2 is cache3
False
```
```Python
>>> from django_redis import get_redis_connection
>>>
>>> redis_client = get_redis_connection()
>>> redis_client.hgetall()
```
### 日志
#### 日志级别
NOTSET < DEBUG < INFO < WARNING < ERROR < FETAL
NOTSET < DEBUG < INFO < WARNING < ERROR < FATAL
#### 日志配置
```Python
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
# 配置日志格式化器
'formatters': {
'simple': {
'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s',
......@@ -810,20 +872,29 @@ LOGGING = {
'datefmt': '%Y-%m-%d %H:%M:%S',
}
},
# 配置日志过滤器
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# 配置日志处理器
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
'filters': ['require_debug_true'],
'formatter': 'simple',
},
'inf': {
'file1': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'info.log',
'filename': 'fang.log',
'when': 'W0',
'backupCount': 12,
'formatter': 'simple',
'level': 'INFO',
},
'err': {
'file2': {
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': 'error.log',
'when': 'D',
......@@ -832,21 +903,13 @@ LOGGING = {
'level': 'WARNING',
},
},
# 配置日志器
'loggers': {
'django': {
'handlers': ['console'],
'level': 'DEBUG',
},
'inform': {
'handlers': ['inf'],
'level': 'DEBUG',
'handlers': ['console', 'file1', 'file2'],
'propagate': True,
},
'error': {
'handlers': ['err'],
'level': 'DEBUG',
'propagate': True,
}
},
}
}
```
......@@ -858,7 +921,6 @@ LOGGING = {
1. Linux相关命令:head、tail、grep、awk、uniq、sort
```Shell
tail -10000 access.log | awk '{print $1}' | uniq -c | sort -r
```
......@@ -868,9 +930,9 @@ LOGGING = {
4. [《集中式日志系统ELK》](https://www.ibm.com/developerworks/cn/opensource/os-cn-elk/index.html)。
- ElasticSearch
- Logstash
- Kibana
- ElasticSearch:搜索引擎,实现全文检索。
- Logstash:负责从指定节点收集日志。
- Kibana:日志可视化工具。
### RESTful
......@@ -889,83 +951,166 @@ LOGGING = {
推荐阅读:[《如何有效防止API的重放攻击》](https://help.aliyun.com/knowledge_detail/50041.html)。
#### 修改配置文件
#### 使用djangorestframework
```Python
安装djangorestfrmework(为了描述方便,以下统一简称为drf)。
```Shell
pip install djangorestframework
```
配置drf。
```Python
INSTALLED_APPS = [
'rest_framework',
]
REST_FRAMEWORK = {
# 配置默认页面大小
'PAGE_SIZE': 5,
# 配置默认的分页类
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
}
```
#### 编写序列化器
```Python
from rest_framework import serializers
class ProvinceSerializer(serializers.ModelSerializer):
class HouseTypeSerializer(serializers.ModelSerializer):
class Meta:
model = Province
fields = ('prov_id', 'prov_name')
model = HouseType
fields = '__all__'
class DistrictSerializer(serializers.ModelSerializer):
class Meta:
model = District
fields = ('distid', 'name', 'intro')
class AgentSerializer(serializers.ModelSerializer):
# 如果属性需要通过代码来获取就定义为SerializerMethodField
# 获取estates属性的方法应该命名为get_estates
estates = serializers.SerializerMethodField()
@staticmethod
def get_estates(agent):
ret_value = []
# 对于多对一外键关联(ForeignKey)可以用select_related提前加载关联属性
# 通过这种方式可以使用内连接或左外连接直接获取关联属性避免1+N查询问题
items = agent.estates.all().select_related('district')
for item in items:
ret_value.append({"estateid": item.estateid,
"name": item.name,
"district": DistrictSerializer(item.district).data})
return ret_value
class Meta:
model = Agent
fields = ('agentid', 'name', 'tel', 'certificated', 'estates')
```
#### 最怂的做法
#### 方法1:使用装饰器
```Python
@api_view(['GET'])
@cache_page(timeout=None)
def provinces(request):
query_set = District.objects.filter(parent__isnull=True)
serializer = DistrictSerializer(query_set, many=True)
return JsonResponse(serializer.data, safe=False)
@csrf_exempt
def list_provinces(request):
if request.method == 'GET':
serializer = ProvinceSerializer(Province.objects.all(), many=True)
elif request.method = 'POST':
serializer = ProvinceSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return HttpResponse(json.dumps(serializer.data),
content_type="application/json; charset=utf-8")
@api_view(['GET'])
@cache_page(timeout=120)
def districts(request, pid):
query_set = District.objects.filter(parent__distid=pid)
serializer = DistrictSerializer(query_set, many=True)
return JsonResponse(serializer.data, safe=False)
```
```Python
urlpatterns = [
path('districts/', views.provinces, name='districts'),
path('districts/<int:pid>', views.districts, name='district'),
]
```
#### 使用装饰器
> 说明:上面使用了Django自带的视图装饰器(@cache_page)来实现对API接口返回数据的缓存。
#### 方法2:使用APIView及其子类
更好的复用代码,不要重“复发明轮子”。
```Python
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from rest_framework_extensions.cache.decorators import cache_response
@api_view(['GET', 'POST'])
def list_provinces(request):
if request.method == 'GET':
serializer = ProvinceSerializer(Province.objects.all(), many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = ProvinceSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def customize_cache_key(view_instance, view_method, request, args, kwargs):
"""自定义缓存的key的函数"""
full_path = request.get_full_path()
return f'fangall:api:{full_path}'
@api_view(['GET'])
def list_cities_by_prov(request, prov_id):
serializer = CitySerializer(City.objects.filter(prov__prov_id=prov_id), many=True)
return Response(serializer.data)
class AgentDetailView(ListAPIView):
queryset = Agent.objects.all()
serializer_class = AgentDetailSerializer
pagination_class = None
@cache_response(key_func=customize_cache_key)
def get(self, request, agentid, *args, **kwargs):
query_set = Agent.objects.filter(agentid=agentid)\
.prefetch_related("estates").last()
serializer = AgentDetailSerializer(query_set)
return Response(serializer.data)
```
```Python
urlpatterns = [
path('provinces/', views.list_provinces),
path('provinces/<int:prov_id>', views.list_cities_by_prov),
path('agents/<int:agentid>', views.AgentDetailView.as_view(), name='agent'),
]
```
> 说明:上面使用了drf_extensions提供的@cache_response实现了对接口数据的缓存,并使用自定义的函数来生成缓存中的key。
#### 方法3:使用ViewSet及其子类
```Python
class HouseTypeViewSet(CacheResponseMixin, viewsets.ModelViewSet):
queryset = HouseType.objects.all()
serializer_class = HouseTypeSerializer
pagination_class = None
```
```Python
router = DefaultRouter()
router.register('housetypes', views.HouseTypeViewSet)
urlpatterns += router.urls
```
> 说明:上面使用了drf_extensions提供的CacheResponseMixin混入类实现了对接口数据的缓存。
drf提供了基于Bootstrap定制的页面来显示接口返回的JSON数据,当然也可以使用[POSTMAN](https://www.getpostman.com/)这样的工具对API接口进行测试。
#### 补充说明
在这里顺便提一下跟前端相关的几个问题。
问题1:如何让浏览器能够发起DELETE/PUT/PATCH?
```HTML
<form method="post">
<input type="hidden" name="_method" value="delete">
......@@ -974,13 +1119,11 @@ urlpatterns = [
```
```Python
if request.method == 'POST' and '_method' in request.POST:
request.method = request.POST['_method'].upper()
```
```HTML
<script>
$.ajax({
'url': '/api/provinces',
......@@ -1003,9 +1146,8 @@ if request.method == 'POST' and '_method' in request.POST:
问题2:如何解决多个JavaScript库之间某个定义(如$函数)冲突的问题?
```HTML
<script src="js/jquery.js"></script>
<script src="js/abc.js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/abc.min.js"></script>
<script>
// $已经被后加载的JavaScript库占用了
// 但是可以直接用绑定在window对象上的jQuery去代替$
......@@ -1016,7 +1158,6 @@ if request.method == 'POST' and '_method' in request.POST:
```
```HTML
<script src="js/abc.min.js"></script>
<script src="js/jquery.min.js"></script>
<script>
......@@ -1031,7 +1172,6 @@ if request.method == 'POST' and '_method' in request.POST:
问题3:jQuery对象与原生DOM对象之间如何转换?
```HTML
<button id="okBtn">点我</button>
<script src="js/jquery.min.js"></script>
<script>
......@@ -1043,123 +1183,213 @@ if request.method == 'POST' and '_method' in request.POST:
</script>
```
#### 使用类视图
#### 身份认证
更好的复用代码,不要重“复发明轮子”
查看drf中APIView类的代码可以看出,drf默认的认证方案是 `DEFAULT_AUTHENTICATION_CLASSES`,如果修改authentication_classes就可以自行定制身份认证的方案
```Python
class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
# 此处省略下面的代码
```
class CityView(APIView):
```Python
DEFAULTS = {
# Base API policies
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
'DEFAULT_THROTTLE_CLASSES': (),
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
'DEFAULT_VERSIONING_CLASS': None,
# 此处省略下面的代码
}
```
def get(self, request, pk, format=None):
try:
serializer = CitySerializer(City.objects.get(pk=pk))
return Response(serializer.data)
except City.DoesNotExist:
raise Http404
自定义认证类,继承`BaseAuthentication`并重写`authenticate(self, request)`方法,通过请求中的userid和token来确定用户身份。如果认证成功,该方法应返回一个二元组(用户和令牌的信息),否则产生异常。也可以重写 `authenticate_header(self, request)`方法来返回一个字符串,该字符串将用于`HTTP 401 Unauthorized`响应中的WWW-Authenticate响应头的值。如果未重写该方法,那么当未经身份验证的请求被拒绝访问时,身份验证方案将返回`HTTP 403 Forbidden`响应。
def put(self, request, pk, format=None):
try:
city = City.objects.get(pk=pk)
serializer = CitySerializer(city, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except City.DoesNotExist:
raise Http404
def delete(self, request, pk, format=None):
```Python
class Authentication(BaseAuthentication):
def authenticate(self, request):
try:
city = City.objects.get(pk=pk)
city.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except City.DoesNotExist:
raise Http404
userid = request.GET['userid']
token = request.GET['token']
user = User.objects.filter(userid=userid, token=token).first()
if not user:
raise AuthenticationFailed('用户身份信息认证失败')
return user, user
except KeyError:
raise NotAuthenticated('请提供当前用户身份认证信息')
def authenticate_header(self, request):
pass
```
```Python
使用自定义的认证类。
urlpatterns = [
path('cities/<int:pk>', views.CityView.as_view()),
]
```Python
class AgentDetailView(ListAPIView):
queryset = Agent.objects.all()
serializer_class = AgentDetailSerializer
authentication_classes = [Authentication, ]
pagination_class = None
@cache_response(key_func=customize_cache_key)
def get(self, request, agentid, *args, **kwargs):
query_set = Agent.objects.filter(agentid=agentid)\
.prefetch_related("estates").last()
serializer = AgentDetailSerializer(query_set)
return Response(serializer.data)
```
#### 使用ViewSet
#### 授予权限
权限检查总是在视图的最开始处运行,在任何其他代码被允许进行之前。最简单的权限是允许通过身份验证的用户访问,并拒绝未经身份验证的用户访问,这对应于dfr中的`IsAuthenticated`类,可以用它来取代默认的`AllowAny`类。权限策略可以在Django的drf配置中用`DEFAULT_PERMISSION_CLASSES`全局设置。
```Python
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
```
class DistrictViewSet(viewsets.ReadOnlyModelViewSet):
也可以在基于`APIView`类的视图上设置身份验证策略。
queryset = District.objects.all()
serializer_class = DistrictSerializer
```Python
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class ExampleView(APIView):
permission_classes = (IsAuthenticated, )
# 此处省略其他代码
```
或者在基于`@api_view`装饰器的视图函数上设置。
```Python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
class DistrictViewSet(viewsets.ModelViewSet):
@api_view(['GET'])
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
# 此处省略其他代码
```
自定义权限需要继承`BasePermission`并实现以下方法中的一个或两个,下面是BasePermission的代码。
queryset = District.objects.all()
serializer_class = DistrictSerializer
```Python
@six.add_metaclass(BasePermissionMetaclass)
class BasePermission(object):
"""
A base class from which all permission classes should inherit.
"""
def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
```
如果请求被授予访问权限,则方法应该返回True,否则返False。下面的例子演示了阻止黑名单中的IP地址访问接口数据(这个在反爬虫的时候很有用哟)。
```Python
from rest_framework import permissions
router = routers.DefaultRouter()
router.register('districts', views.DistrictViewSet)
urlpatterns += router.urls
class BlacklistPermission(permissions.BasePermission):
"""
Global permission check for blacklisted IPs.
"""
def has_permission(self, request, view):
ip_addr = request.META['REMOTE_ADDR']
blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists()
return not blacklisted
```
#### 认证用户身份
如果要实现更为完整的权限验证,可以考虑RBAC或ACL。
1. 利用Django自带的User。
1. RBAC - 基于角色的访问控制(用户-角色-权限-资源,都是多对多关系)。
2. ACL - 访问控制列表(每个用户绑定自己的访问白名单)。
2. 自行对用户及其身份验证的摘要进行处理。
#### 访问限流
```Python
sha1_proto = hashlib.sha1()
def check_user_sign(get_response):
def middleware(request):
if request.path.startswith('/api'):
data = request.GET if request.method == 'GET' else request.data
try:
user_id = data['user_id']
user_token = cache.get(f'fang:user:{user_id}')
user_sign = data['user_sign']
hasher = sha1_proto.copy()
hasher.update(f'{user_id}:{user_token}'.encode('utf-8'))
if hasher.hexdigest() == user_sign:
return get_response(request)
except KeyError:
pass
return JsonResponse({'msg': '身份验证失败拒绝访问'})
else:
return get_response(request)
return middleware
```
可以修改dfr配置的`DEFAULT_THROTTLE_CLASSES` 和 `DEFAULT_THROTTLE_RATES`两个值来设置全局默认限流策略。例如:
3. 请求的时效性问题。(请求中再加上一个随机的令牌)
```Python
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
),
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}
}
```
### 其他问题
`DEFAULT_THROTTLE_RATES`中使用的频率描述可能包括`second`、`minute`、`hour`或`day`。
问题1:如何设计一套权限系统?(RBAC或ACL)
如果要为接口单独设置限流,可以在每个视图或视图集上设置限流策略,如下所示:
1. RBAC - 基于角色的访问控制(用户-角色-权限,都是多对多关系)。
2. ACL - 访问控制列表(每个用户绑定自己的访问白名单)。
```Python
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
class ExampleView(APIView):
throttle_classes = (UserRateThrottle, )
# 此处省略下面的代码
```
问题2:如何实现异步任务和定时任务?(Celery)
问题3:如何解决JavaScript跨域获取数据的问题?(django-cors-headers)
```Python
@api_view(['GET'])
@throttle_classes([UserRateThrottle, ])
def example_view(request, format=None):
# 此处省略下面的代码
```
问题4:网站图片(水印、剪裁)和视频(截图、水印、转码)是如何处理的?(云存储、FFmpeg)
当然也可以通过继承`BaseThrottle`来自定义限流策略,需要重写`allow_request`和`wait`方法。
问题5:网站如何架设(静态资源)文件系统?
### 异步任务和计划任务
#### Celery的应用
......@@ -1174,7 +1404,6 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis
1. 安装RabbitMQ。
```Shell
docker pull rabbitmq
docker run -d -p 5672:5672 --name myrabbit rabbitmq
docker container exec -it myrabbit /bin/bash
......@@ -1183,18 +1412,16 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis
2. 创建用户、资源以及分配操作权限。
```Shell
rabbitmqctl add_user jackfrued 123456
rabbitmqctl set_user_tags jackfrued administrator
rabbitmqctl add_vhost myvhost
rabbitmqctl set_permissions -p myvhost jackfrued ".*" ".*" ".*"
rabbitmqctl add_user luohao 123456
rabbitmqctl set_user_tags luohao administrator
rabbitmqctl add_vhost vhost1
rabbitmqctl set_permissions -p vhost1 luohao ".*" ".*" ".*"
```
3. 创建Celery实例。
```Python
project_name = 'fang'
project_name = '...'
project_settings = '%s.settings' % project_name
# 注册环境变量
......@@ -1202,8 +1429,7 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis
app = celery.Celery(
project_name,
backend='amqp://jackfrued:123456@120.77.222.217:5672/myvhost',
broker='amqp://jackfrued:123456@120.77.222.217:5672/myvhost'
broker='amqp://luohao:123456@120.77.222.217:5672/vhost1'
)
# 从默认的配置文件读取配置信息
......@@ -1213,53 +1439,82 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
```
4. 启动Celery创建woker。
4. 启动Celery创建woker(消息的消费者)
```Shell
celery -A fang worker -l info &
celery -A <projname> worker -l debug &
```
5. 执行异步任务。
```Python
@app.task
def send_email(from, to, cc, subject, content):
pass
async_result = send_email.delay('', [], [], '', '')
async_result.get()
send_email.delay('', [], [], '', '')
```
6. 创建定时任务。
```Python
from celery.schedules import crontab
from celery.task import periodic_task
@periodic_task(run_every=crontab('*', '12,18'))
def print_dummy_info():
print('你妈喊你回家吃饭啦')
# 配置定时任务(计划任务)
app.conf.update(
timezone=settings.TIME_ZONE,
enable_utc=True,
# 定时任务(计划任务)相当于是消息的生产者
# 如果只有生产者没有消费者那么消息就会在消息队列中积压
# 将来实际部署项目的时候生产者、消费者、消息队列可能都是不同节点
beat_schedule={
'task1': {
'task': 'common.tasks.show_msg',
'schedule': crontab(),
'args': ('刘强东,奶茶妹妹喊你回家喝奶啦', )
},
},
)
```
7. 检查定时任务并交给worker执行。
```Python
@app.task
def show_msg(content):
print(content)
```
7. 启动Celery创建执行定时任务的beat(消息的生产者)。
```Shell
celery -A fang beat -l info
celery -A <projname> beat -l info
```
8. 检查消息队列状况。
```Shell
rabbitmqctl list_queues -p myvhost
rabbitmqctl list_queues -p vhost1
```
### 其他问题
问题1:如何解决JavaScript跨域获取数据的问题?(django-cors-headers)
```Python
INSTALLED_APPS = [
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
]
# 配置跨域白名单
# CORS_ORIGIN_WHITELIST = ()
# 配置是否跨域读取Cookie信息
# CORS_ALLOW_CREDENTIALS = True
```
问题2:网站图片(水印、剪裁)和视频(截图、水印、转码)是如何处理的?(云存储、FFmpeg)
问题3:网站如何架设(静态资源)文件系统?(FastDFS、云存储、CDN)
### 安全保护
问题1:什么是跨站脚本攻击,如何防范?(对提交的内容进行消毒)
......@@ -1272,51 +1527,48 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis
#### Django提供的安全措施
签名数据的API
签名数据的API
```Python
>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign('hello, world!')
>>> value
'hello, world!:BYMlgvWMTSPLxC-DqxByleiMVXU'
>>> signer.unsign(value)
'hello, world!'
>>>
>>> signer = Signer(salt='1qaz2wsx')
>>> signer.sign('hello, world!')
'hello, world!:9vEvG6EA05hjMDB5MtUr33nRA_M'
>>>
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign('hello, world!')
>>> value
'hello, world!:1fpmcQ:STwj464IFE6eUB-_-hyUVF3d2So'
>>> signer.unsign(value, max_age=5)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/Hao/Desktop/fang.com/venv/lib/python3.6/site-packages/django/core/signing.py", line 198, in unsign
'Signature age %s > %s seconds' % (age, max_age))
django.core.signing.SignatureExpired: Signature age 21.020604848861694 > 5 seconds
>>> signer.unsign(value, max_age=120)
'hello, world!'
>>> from django.core.signing import Signer
>>> signer = Signer()
>>> value = signer.sign('hello, world!')
>>> value
'hello, world!:BYMlgvWMTSPLxC-DqxByleiMVXU'
>>> signer.unsign(value)
'hello, world!'
>>>
>>> signer = Signer(salt='1qaz2wsx')
>>> signer.sign('hello, world!')
'hello, world!:9vEvG6EA05hjMDB5MtUr33nRA_M'
>>>
>>> from django.core.signing import TimestampSigner
>>> signer = TimestampSigner()
>>> value = signer.sign('hello, world!')
>>> value
'hello, world!:1fpmcQ:STwj464IFE6eUB-_-hyUVF3d2So'
>>> signer.unsign(value, max_age=5)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/Hao/Desktop/fang.com/venv/lib/python3.6/site-packages/django/core/signing.py", line 198, in unsign
'Signature age %s > %s seconds' % (age, max_age))
django.core.signing.SignatureExpired: Signature age 21.020604848861694 > 5 seconds
>>> signer.unsign(value, max_age=120)
'hello, world!'
```
5. CSRF令牌和小工具
CSRF令牌和小工具
```HTML
{% csrf_token %}
```
```HTML
{% csrf_token %}
```
```Python
@csrf_exempt
@csrf_protect
@require_csrf_token
@ensure_csrf_cookie
```
- @csrf_exempt:免除令牌
- @csrf_protect:提供令牌保护
- @require_csrf_token:提供令牌保护
- @ensure_csrf_cookie:强制视图发送带令牌的cookie
> 说明:可以在Chrome浏览器中安装EditThisCookie插件来方便的查看Cookie。
#### 用户敏感信息的保护
......@@ -1324,26 +1576,28 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis
1. 哈希摘要(签名)
```Python
>>> import hashlib
>>>
>>> md5_proto = hashlib.md5()
>>> md5_hasher = md5_proto.copy()
>>> md5_hasher = hashlib.md5()
>>> md5_hasher.update('hello, world!'.encode())
>>> md5_hasher.hexdigest()
'3adbbad1791fbae3ec908894c4963870'
>>>
>>> sha1_proto = hashlib.sha1()
>>> sha1_hasher = sha1_proto.copy()
>>> sha1_hasher = hashlib.sha1()
>>> sha1_hasher.update('hello, world!'.encode())
>>> sha1_hasher.update('goodbye, world!'.encode())
>>> sha1_hasher.hexdigest()
'1f09d30c707d53f3d16c530dd73d70a6ce7596a9'
```
2. 加密和解密(对称加密和非对称加密)
2. 加密和解密(对称加密[AES]和非对称加密[RSA])
```Shell
pip install rsa
pip install pycrypto
```
```Python
>>> pub_key, pri_key = rsa.newkeys(1024)
>>> message = 'hello, world!'
>>> crypto = rsa.encrypt(message.encode(), pub_key)
......@@ -1364,17 +1618,44 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis
### 测试相关
1. 测试为项目带来的好处有哪些?(更早的发现问题,更好的团队协作)
2. 什么是黑盒测试和白盒测试?(方法是黑盒子,不知道实现细节,通过设定输入和预期输出进行来断言方法是否实现了应有的功能)
测试是发现和标记缺陷的过程。所谓的缺陷是指实际结果和期望结果之间的任何差别。有的地方,测试也被认为是执行以找出错误为目的的程序的过程。 测试是为了让产品达到以下目标:
1. 满足需求用户满意
2. 改善产品的市场占有率
3. 树立对产品的信任
4. 减少开发和维护的成本
#### 功能测试
如果一个软件单元的行为方式与它的开发规范完全一样,那么该软件单元就通过了它的功能测试。
- 白盒测试:开发人员自己实现,最基本的形式是单元测试,还有集成测试和系统测试。
- 黑盒测试:由开发团队之外的人执行,对测试代码没有可见性,将被测系统视为黑盒子。通常由测试人员或QA工程师来执行,Web应用可以通过Selenium这样的测试框架自动化实施。
#### 性能测试
软件在高工作负载下对其响应性和健壮性展开的测试。
- 负载测试:在特定负载下执行的测试。
- 压力测试:突发条件或极限条件下的性能测试。
#### 安全性测试
系统的敏感数据都是经过认证和授权之后才能访问。
#### 其他测试
易用性测试 / 安装测试 / 可访问性测试
#### 单元测试
目标:测试函数和对象的方法(程序中最基本的单元)
测试函数和对象的方法(程序中最小最基本的单元)。通过对实际输出和预期输出的比对以及各种的断言条件来判定被测单元是否满足设计需求
实施:通过对实际输出和预期输出的比对以及各种的断言条件来判定被测单元是否满足设计需求。
- 测试用例
- 测试固件 - 每次测试时都要使用的东西。
- 测试套件(测试集)- 组合了多个测试用例而构成的集合。
```Python
class UtilTest(TestCase):
def setUp(self):
......@@ -1394,57 +1675,185 @@ class UtilTest(TestCase):
self.assertEqual(value, to_md5_hex(key))
```
#### 测试模型
`TestCase`的断言方法:
- assertEqual / assertNotEqual
- assertTrue / assertFalse / assertIsNot
- assertRaise / assertRaiseRegexp
- assertAlmostEqual / assertNotAlmostEqual
- assertGreater / assertGreaterEqual / assertLess / assertLessEqual
- assertRegexpMatches / assertNotRegexpMatches
- assertListEqual / assertSetEqual / assertTupleEqual / assertDictEqual
可以使用nose2或pytest来辅助执行单元测试,同时通过cov-core或pytest-cov可以对测试覆度进行评估。覆盖率由百分比表示。比如测试代码执行过了程序的每一行,那么覆盖率就是100%。这种时候,几乎不会出现新程序上线后突然无法运行的尴尬情况。覆盖率不关心代码内容究竟是什么,覆盖率是用来检查“测试代码不足、测试存在疏漏”的一个指标,“测试内容是否妥当”并不归它管。
```Shell
pip install nose2 pytest cov-core pytest-cov
```
可以使用Selenium来实现Web应用的自动化测试,它还可以用于屏幕抓取与浏览器行为模拟,通过爬虫抓取页面上的动态数据也可以使用它。Selenium其实包括三个部分:
- Selenium IDE:嵌入到浏览器的插件,可以录制和回放脚本。
![](./res/selenium_ide.png)
- Selenium WebDriver:支持多种语言可以操控浏览器的API。
- Selenium Standalone Server:Selenium Grid、远程控制、分布式部署。
```Shell
pip install selenium
```
```Python
from selenium import webdriver
import pytest
import contextlib
class ModelTest(TestCase):
def test_save_province(self):
pass
@pytest.fixture(scope='session')
def chrome():
# 设置使用无头浏览器(不会打开浏览器窗口)
options = webdriver.ChromeOptions()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
yield driver
driver.quit()
def test_del_province(self):
pass
def test_update_province(self):
pass
def test_baidu_index(chrome):
chrome.get('https://www.baidu.com')
assert chrome.title == '百度一下,你就知道'
def test_get_all_provinces(self):
pass
```
def test_get_province_by_id(self):
pass
```Shell
nose2 -v -C
pytest --cov
```
```Shell
Ran 7 tests in 0.002s
OK
Name Stmts Miss Cover
----------------------------------------------
example01.py 15 0 100%
example02.py 49 49 0%
example03.py 22 22 0%
example04.py 61 61 0%
example05.py 29 29 0%
example06.py 39 39 0%
example07.py 19 19 0%
example08.py 27 27 0%
example09.py 18 18 0%
example10.py 19 19 0%
example11.py 22 22 0%
example12.py 28 28 0%
example13.py 28 28 0%
test_ddt_example.py 18 0 100%
test_pytest_example.py 11 6 45%
test_unittest_example.py 22 0 100%
----------------------------------------------
TOTAL 427 367 14%
```
在测试过程中需要孤立各种外部依赖(数据库、外部接口调用、时间依赖),具体又包括两个方面:
1. 数据源:数据本地化 / 置于内存中 / 测试之后回滚
2. 资源虚拟化:存根/桩(stub)、仿制/模拟(mock)、伪造(fake)
- stub:测试期间为提供响应的函数生成的替代品
- mock:代替实际对象(以及该对象的API)的对象
- fake:没有达到生产级别的轻量级对象
#### 集成测试
集成多个函数或方法的输入输出的测试,测试时需要将多个测试对象组合在一起。
- 测试组件互操作性 / 需求变更测试 / 外部依赖和API / 调试硬件问题 / 在代码路径中发现异常
#### 系统测试
对需求的测试,测试成品是否最终满足了所有需求,在客户验收项目时进行。
#### 数据驱动测试
使用外部数据源实现对输入值与期望值的参数化,避免在测试中使用硬编码的数据。
被测函数:
```Python
def add(x, y):
return x + y
```
data.csv文件:
```
3,1,2
0,1,-1
100,50,50
100,1,99
15,7,8
```
#### 测试视图
测试代码:
```Python
import csv
class RentViewTest(TestCase):
from unittest import TestCase
from ddt import ddt, data, unpack
def test_index_view(self):
resp = self.client.get(reverse('index'))
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.context['num'], 5)
@ddt
class TestAdd(TestCase):
def load_data_from_csv(filename):
data_items = []
with open(filename, 'r', newline='') as fs:
reader = csv.reader(fs)
for row in reader:
data_items.append(list(map(int, row)))
return data_items
@data(*load_data_from_csv('data.csv'))
@unpack
def test_add(self, result, param1, param2):
self.assertEqual(result, add(param1, param2))
```
#### 运行测试
#### Django中的测试
配置测试数据库
1. 测试Django视图 - Django中提供的`TestCase`扩展了`unittest`中的`TestCase`,绑定了一个名为`client`的属性,可以用来模拟浏览器发出的GET、POST、DELETE、PUT等请求。
```Python
class SomeViewTest(TestCase):
def test_example_view(self):
resp = self.client.get(reverse('index'))
self.assertEqual(200, resp.status_code)
self.assertEqual(5, resp.context['num'])
```
2. 运行测试 - 配置测试数据库。
```Python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': 'localhost',
'PORT': 3306,
'NAME': 'House',
'NAME': 'DbName',
'USER': os.environ['DB_USER'],
'PASSWORD': os.environ['DB_PASS'],
'TEST': {
'NAME': 'House_for_testing',
'NAME': 'DbName_for_testing',
'CHARSET': 'utf8',
},
}
......@@ -1452,17 +1861,15 @@ DATABASES = {
```
```Shell
python manage.py test
python manage.py test common
python manage.py test common.tests.UtilsTest
python manage.py test common.tests.UtilsTest.test_to_md5_hex
```
#### 测试覆盖度
3. 评估测试覆盖度
```Shell
pip install coverage
coverage run --source=<path1> --omit=<path2> manage.py test common
coverage report
......@@ -1489,12 +1896,12 @@ TOTAL 267 176 34%
#### 性能测试
问题1:性能测试的指标有哪些?()
问题1:性能测试的指标有哪些?
1. ab
1. ab - Apache Benchmark / webbench / httpperf
```Shell
yum -y install httpd
ab -c 10 -n 1000 http://www.baidu.com/
...
Benchmarking www.baidu.com (be patient).....done
......@@ -1536,7 +1943,6 @@ TOTAL 267 176 34%
2. mysqlslap
```Shell
mysqlslap -a -c 100 -h 1.2.3.4 -u root -p
mysqlslap -a -c 100 --number-of-queries=1000 --auto-generate-sql-load-type=read -h <负载均衡服务器IP地址> -u root -p
mysqlslap -a --concurrency=50,100 --number-of-queries=1000 --debug-info --auto-generate-sql-load-type=mixed -h 1.2.3.4 -u root -p
......@@ -1545,7 +1951,6 @@ TOTAL 267 176 34%
3. sysbench
```Shell
sysbench --test=threads --num-threads=64 --thread-yields=100 --thread-locks=2 run
sysbench --test=memory --num-threads=512 --memory-block-size=256M --memory-total-size=32G run
```
......@@ -1554,7 +1959,50 @@ TOTAL 267 176 34%
请查看[《使用JMeter进行性能测试》](https://www.ibm.com/developerworks/cn/java/l-jmeter/index.html)。
5. LoadRunner
5. LoadRunner / QTP
### 项目调试
可以使用django-debug-toolbar来辅助项目调试。
1. 安装
```Shell
pip install django-debug-toolbar
```
2. 配置 - 修改settings.py。
```Python
INSTALLED_APPS = [
'debug_toolbar',
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
DEBUG_TOOLBAR_CONFIG = {
# 引入jQuery库
'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js',
# 工具栏是否折叠
'SHOW_COLLAPSED': True,
# 是否显示工具栏
'SHOW_TOOLBAR_CALLBACK': lambda x: True,
}
```
3. 配置 - 修改urls.py。
```Python
if settings.DEBUG:
import debug_toolbar
urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls)))
```
4. 使用 - 在页面右侧可以看到一个调试工具栏,上面包括了执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等调试信息,查看起来非常的方便。
### 部署相关
......@@ -1579,7 +2027,6 @@ TOTAL 267 176 34%
2. 开启[模板缓存](https://docs.djangoproject.com/en/2.0/ref/templates/api/#django.template.loaders.cached.Loader)来加速模板的渲染。
```Python
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
......@@ -1611,8 +2058,11 @@ TOTAL 267 176 34%
1. 用ID生成器代替自增主键(性能更好、适用于分布式环境)。
- 自定义ID生成器
- UUID
```Python
>>> my_uuid = uuid.uuid1()
>>> my_uuid
UUID('63f859d0-a03a-11e8-b0ad-60f81da8d840')
......@@ -1622,22 +2072,29 @@ TOTAL 267 176 34%
2. 避免不必要的外键列上的约束(除非必须保证参照完整性),更不要使用触发器之类的机制。
3. 使用索引来优化查询性能(索引放在要用于查询的字段上)。
3. 使用索引来优化查询性能(索引放在要用于查询的字段上)。InnoDB用的是BTREE索引,使用>、<、>=、<=、BETWEEN或者LIKE 'pattern'(pattern不以通配符开头)时都可以用到索引。因为建立索引需要额外的磁盘空间,而主键上是有默认的索引,所以主键要尽可能选择较短的数据类型来减少磁盘占用,提高索引的缓存效果。
```SQL
create index idx_goods_name on tb_goods (gname(10));
```
```SQL
-- 无法使用索引
select * from tb_goods where gname like '%iPhone%';
-- 可以使用索引
select * from tb_goods where gname like 'iPhone%';
```
```Python
# 无法使用索引
Goods.objects.filter(name_icontains='iPhone')
# 可以使用索引
Goods.objects.filter(name__istartswith='iPhone');
```
4. 使用存储过程(存储在服务器端编译过的一组SQL语句)。
```SQL
drop procedure if exists sp_avg_sal_by_dept;
create procedure sp_avg_sal_by_dept(deptno integer, out avg_sal float)
......@@ -1651,7 +2108,6 @@ TOTAL 267 176 34%
```
```Python
>>> from django.db import connection
>>> cursor = connection.cursor()
>>> cursor.callproc('sp_avg_sal_by_dept', (10, 0))
......@@ -1660,19 +2116,70 @@ TOTAL 267 176 34%
(2675.0,)
```
5. 使用`explain`来分析查询性能 - 执行计划。
5. 使用数据分区。通过分区可以存储更多的数据、优化查询更大的吞吐量、可以快速删除过期的数据。关于这个知识点可以看看MySQL的[官方文档](https://dev.mysql.com/doc/refman/5.7/en/partitioning-overview.html)。
- RANGE分区:基于连续区间范围,把数据分配到不同的分区。
- LIST分区:基于枚举值的范围,把数据分配到不同的分区。
- HASH分区 / KEY分区:基于分区个数,把数据分配到不同的分区。
```SQL
CREATE TABLE tb_emp (
eno INT NOT NULL,
ename VARCHAR(20) NOT NULL,
job VARCHAR(10) NOT NULL,
hiredate DATE NOT NULL,
dno INT NOT NULL
)
PARTITION BY HASH(dno)
PARTITIONS 4;
```
```SQL
CREATE TABLE tb_emp (
eno INT NOT NULL,
ename VARCHAR(20) NOT NULL,
job VARCHAR(10) NOT NULL,
hiredate DATE NOT NULL,
dno INT NOT NULL
)
PARTITION BY RANGE( YEAR(hiredate) ) (
PARTITION p0 VALUES LESS THAN (1960),
PARTITION p1 VALUES LESS THAN (1970),
PARTITION p2 VALUES LESS THAN (1980),
PARTITION p3 VALUES LESS THAN (1990),
PARTITION p4 VALUES LESS THAN MAXVALUE
);
```
6. 使用`explain`来分析查询性能 - 执行计划。
```SQL
explain select * from ...;
```
请参考网易出品的《深入浅出MySQL》上面对应部分的讲解(已经有第二版)。
`explain`结果解析:
- select_type:表示select操作的类型,常见的值有SIMPLE(简单查询,没有使用子查询或者表连接查询)、PRIMARY(主查询,外层的查询)、UNION(并集操作中的第二个或者后面的查询)、SUBQUERY(子查询中的第一个SELECT)等。
- table:输出结果的表。
- type:MySQL在表中找到所需行的方式,也称为访问类型,常见的值有:
- ALL:全表扫描(遍历全表找到匹配的行)
- index:索引全扫描(遍历整个索引)
- range:索引范围扫描
- ref:非唯一索引扫描或唯一索引的前缀扫描
- eq_ref:唯一索引扫描
- const / system:表中最多有一行匹配
- NULL:不用访问表或者索引
- possible_keys:查询时可能用到的索引。
- key:实际使用的索引。
- key_len:使用到索引字段的长度。
- rows:扫描行的数量。
- Extra:额外的信息(执行情况的说明或描述)。
> 说明:关于MySQL更多的知识尤其是性能调优和运维方面的内容,推荐大家阅读网易出品的《深入浅出MySQL(第2版)》,网易出品必属精品。
6. 使用慢查询日志来发现性能低下的查询。
7. 使用慢查询日志来发现性能低下的查询。
```SQL
mysql> show variables like 'slow_query%';
+---------------------------+----------------------------------+
| Variable_name | Value |
......@@ -1690,14 +2197,11 @@ TOTAL 267 176 34%
```
```SQL
mysql> set global slow_query_log='ON';
mysql> set global slow_query_log_file='/usr/local/mysql/data/slow.log';
mysql> set global long_query_time=1;
```
```INI
[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
......@@ -1706,4 +2210,4 @@ TOTAL 267 176 34%
#### 其他
请参考《Python项目性能调优》。
\ No newline at end of file
请参考《Python性能调优》。
\ No newline at end of file
## Docker入门
## Docker简易上手指南
### Docker简介
软件开发中最为麻烦的事情可能就是配置环境了。由于用户使用的操作系统具有多样性,即便使用跨平台的开发语言(如Java和Python)都不能保证代码能够在各种平台下都可以正常的运转,而且可能在不同的环境下我们的软件需要依赖的其他软件包也是不一样的。
那么问题来了,我们再安装软件的时候可不可以把软件运行的环境一并安装?也就是说在安装软件的时候,我们是不是可以把原始环境一模一样地复制过来呢?
那么问题来了,我们再安装软件的时候可不可以把软件运行的环境一并安装?也就是说在安装软件的时候,我们是不是可以把原始环境一模一样地复制过来呢?
虚拟机(virtual machine)就是带环境安装的一种解决方案,它可以在一种操作系统里面运行另一种操作系统,比如在Windows系统里面运行Linux系统,在macOS上运行Windows,而应用程序对此毫无感知。使用过虚拟机的人都知道,虚拟机用起来跟真实系统一模一样,而对于虚拟机的宿主系统来说,虚拟机就是一个普通文件,不需要了就删掉,对宿主系统或者其他的程序并没有影响。但是虚拟机通常会占用较多的系统资源,启动和关闭也非常的缓慢,总之用户体验没有想象中的那么好。
......@@ -87,7 +87,7 @@ docker stop <container-id>
docker stop <name>
```
对于那些不会自动终止的容器,就可以用下面的方式来停止。
对于那些不会自动终止的容器,就可以用下面的方式来停止。
```Shell
docker container kill <container-id>
......@@ -107,10 +107,10 @@ service docker start
```JavaScript
{
"registry-mirrors": [
"http://hub-mirror.c.163.com",
"https://registry.docker-cn.com"
]
"registry-mirrors": [
"http://hub-mirror.c.163.com",
"https://registry.docker-cn.com"
]
}
```
......@@ -195,3 +195,4 @@ select user, host, plugin, authentication_string from user where user='root';
2 rows in set (0.00 sec)
```
接下来就已经可以访问你的MySQL服务器啦,当然远程连接的时候不要忘了在防火墙上开启对应的端口。
\ No newline at end of file
## 项目实战 - 开启团队项目
## 开启团队项目
### 创建项目
......
## 电商网站技术要点剖析
### 商业模式
1. B2B - 商家对商家,交易双方都是企业(商家),最典型的案例就是阿里巴巴。
2. C2C - 个人对个人,例如:淘宝、人人车。
3. B2C - 商家对个人,例如:唯品会,聚美优品。
4. C2B - 个人对商家,先有消费者提出需求,后有商家按需求组织生产,例如: 尚品宅配。
5. O2O - 线上到线下,将线下的商务机会与互联网结合,让互联网成为线下交易的平台,例如:美团外卖、饿了么。
6. B2B2C - 商家对商家对个人,例如:天猫、京东。
### 需求要点
1. 用户端
- 首页(商品分类、广告轮播、滚动快讯、瀑布加载)
- 用户(登录(第三方登录)、注册、注销、自服务(个人信息、浏览历史、收货地址、……))
- 商品(分类、列表、详情、搜索、添加到购物车)
- 购物车(查看、编辑(修改数量、删除商品、清空))
- 订单(提交订单(支付)、历史订单、订单详情、订单评价)
2. 管理端
> 提示:可以通过思维导图来进行需求的整理,思维导图上的每个叶子节点都是不可再拆分的功能。
### 物理模型设计
两个概念:SPU(Standard Product Unit)和SKU(Stock Keeping Unit)。
- SPU:iPhone 6s
- SKU:iPhone 6s 64G 土豪金
![](./res/shopping-pdm.png)
### 第三方登录
第三方登录是指利用第三方网站(通常是知名社交网站)的账号进行登录验证,比如国内的 QQ、微博,国外的Google、Facebook等,第三方登录大部分都是使用[OAuth](),它是一个关于授权的开放网络标准,得到了广泛的应用,目前通常使用的是2.0版本。关于OAuth的基础知识,可以阅读阮一峰老师的[《理解OAuth 2.0》](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)。
#### OAuth 2.0授权流程
1. 用户打开客户端以后,客户端要求用户(资源所有者)给予授权。
2. 用户(资源所有者)同意给予客户端授权。
3. 客户端使用上一步获得的授权,向认证服务器申请访问令牌。
4. 认证服务器对客户端进行认证以后,发放访问令牌。
5. 客户端使用访问令牌向资源服务器申请获取资源。
6. 资源服务器确认访问令牌无误,同意向客户端开放资源。
![](./res/oauth2.png)
如果使用微博登录进行接入,其具体步骤可以参考微博开放平台上的[“微博登录接入”](http://open.weibo.com/wiki/Connect/login)文档。使用QQ登录进行接入,需要首先注册成为QQ互联开发者并通过审核,具体的步骤可以参考QQ互联上的[“接入指南”](http://wiki.connect.qq.com/),具体的步骤可以参考[“网站开发流程”](http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0)
> 提示:在Gitbook上面有一本名为[《Django博客入门》](https://shenxgan.gitbooks.io/django/content/publish/2015-08-10-django-oauth-login.html)的书以Github为例介绍了第三方账号登录,有兴趣的可以自行阅读。
通常电商网站在使用第三方登录时,会要求与网站账号进行绑定或者根据获取到的第三方账号信息(如:手机号)自动完成账号绑定。
### 缓存预热和查询缓存
#### 缓存预热
所谓缓存预热,是指在启动服务器时将数据提前加载到缓存中,为此可以在Django应用的`apps.py`模块中编写`AppConfig`的子类并重写`ready()`方法,代码如下所示。
```Python
import pymysql
from django.apps import AppConfig
from django.core.cache import cache
SELECT_PROVINCE_SQL = 'select distid, name from tb_district where pid is null'
class CommonConfig(AppConfig):
name = 'common'
def ready(self):
conn = pymysql.connect(host='1.2.3.4', port=3306,
user='root', password='pass',
database='db', charset='utf8',
cursorclass=pymysql.cursors.DictCursor)
try:
with conn.cursor() as cursor:
cursor.execute(SELECT_PROVINCE_SQL)
provinces = cursor.fetchall()
cache.set('provinces', provinces)
finally:
conn.close()
```
接下来,还需要在应用的`__init__.py`中编写下面的代码。
```Python
default_app_config = 'common.apps.CommonConfig'
```
或者在项目的`settings.py`文件中注册应用。
```Python
INSTALLED_APPS = [
...
'common.apps.CommonConfig',
...
]
```
#### 查询缓存
```Python
from pickle import dumps
from pickle import loads
from django.core.cache import caches
MODEL_CACHE_KEY = 'project:modelcache:%s'
def my_model_cache(key, section='default', timeout=None):
"""实现模型缓存的装饰器"""
def wrapper1(func):
def wrapper2(*args, **kwargs):
real_key = '%s:%s' % (MODEL_CACHE_KEY % key, ':'.join(map(str, args)))
serialized_data = caches[section].get(real_key)
if serialized_data:
data = loads(serialized_data)
else:
data = func(*args, **kwargs)
cache.set(real_key, dumps(data), timeout=timeout)
return data
return wrapper2
return wrapper1
```
```Python
@my_model_cache(key='provinces')
def get_all_provinces():
return list(Province.objects.all())
```
### 购物车实现
问题一:已登录用户的购物车放在哪里?未登录用户的购物车放在哪里?
```Python
class CartItem(object):
"""购物车中的商品项"""
def __init__(self, sku, amount=1, selected=False):
self.sku = sku
self.amount = amount
self.selected = selected
@property
def total(self):
return self.sku.price * self.amount
class ShoppingCart(object):
"""购物车"""
def __init__(self):
self.items = {}
self.index = 0
def add_item(self, item):
if item.sku.id in self.items:
self.items[item.sku.id].amount += item.amount
else:
self.items[item.sku.id] = item
def remove_item(self, sku_id):
if sku_id in self.items:
self.items.remove(sku_id)
def clear_all_items(self):
self.items.clear()
@property
def cart_items(self):
return self.items.values()
@property
def cart_total(self):
total = 0
for item in self.items.values():
total += item.total
return total
```
已登录用户的购物车可以放在数据库中(可以先在Redis中缓存);未登录用户的购物车可以保存在Cookie中(减少服务器端内存开销)。
```JSON
{
'1001': {sku: {...}, 'amount': 1, 'selected': True},
'1002': {sku: {...}, 'amount': 2, 'selected': False},
'1003': {sku: {...}, 'amount': 3, 'selected': True},
}
```
```Python
request.get_signed_cookie('cart')
cart_base64 = base64.base64encode(pickle.dumps(cart))
response.set_signed_cookie('cart', cart_base64)
```
问题二:用户登录之后,如何合并购物车?(目前电商应用的购物车几乎都做了持久化处理,主要是方便在多个终端之间共享数据)
### 集成支付功能
问题一:支付信息如何持久化?(必须保证每笔交易都有记录)
问题二:如何接入支付宝?(接入其他平台基本类似)
1. [蚂蚁金服开放平台](https://open.alipay.com/platform/home.htm)
2. [入驻平台](https://open.alipay.com/platform/homeRoleSelection.htm)
3. [开发者中心](https://openhome.alipay.com/platform/appManage.htm#/apps)
4. [文档中心](https://docs.open.alipay.com/270/105899/)
5. [SDK集成](https://docs.open.alipay.com/54/103419) - [PYPI链接](https://pypi.org/project/alipay-sdk-python/)
6. [API列表](https://docs.open.alipay.com/270/105900/)
![](./res/alipay_web_developer.png)
配置文件:
```Python
ALIPAY_APPID = '......'
ALIPAY_URL = 'https://openapi.alipaydev.com/gateway.do'
ALIPAY_DEBUG = False
```
获得支付链接(发起支付):
```Python
# 创建调用支付宝的对象
alipay = AliPay(
# 在线创建应用时分配的ID
appid=settings.ALIPAY_APPID,
app_notify_url=None,
# 自己应用的私钥
app_private_key_path=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'keys/app_private_key.pem'),
# 支付宝的公钥
alipay_public_key_path=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'keys/alipay_public_key.pem'),
sign_type='RSA2',
debug=settings.ALIPAY_DEBUG
)
# 调用获取支付页面操作
order_info = alipay.api_alipay_trade_page_pay(
out_trade_no='...',
total_amount='...',
subject='...',
return_url='http://...'
)
# 生成完整的支付页面URL
alipay_url = settings.ALIPAY_URL + '?' + order_info
return JsonResponse({'alipay_url': alipay_url})
```
通过上面返回的链接可以进入支付页面,支付完成后会自动跳转回上面代码中设定好的项目页面,在该页面中可以获得订单号(out_trade_no)、支付流水号(trade_no)、交易金额(total_amount)和对应的签名(sign)并请求后端验证和保存交易结果,代码如下所示:
```Python
# 创建调用支付宝的对象
alipay = AliPay(
# 在线创建应用时分配的ID
appid=settings.ALIPAY_APPID,
app_notify_url=None,
# 自己应用的私钥
app_private_key_path=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'keys/app_private_key.pem'),
# 支付宝的公钥
alipay_public_key_path=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'keys/alipay_public_key.pem'),
sign_type='RSA2',
debug=settings.ALIPAY_DEBUG
)
# 请求参数(假设是POST请求)中包括订单号、支付流水号、交易金额和签名
params = request.POST.dict()
# 调用验证操作
if alipay.verify(params, params.pop('sign')):
# 对交易进行持久化操作
```
支付宝的支付API还提供了交易查询、交易结算、退款、退款查询等一系列的接口,可以根据业务需要进行调用,此处不再进行赘述。
### 秒杀和超卖
1. 秒杀:秒杀是通常意味着要在很短的时间处理极高的并发,系统在短时间需要承受平时百倍以上的流量,因此秒杀架构是一个比较复杂的问题,其核心思路是流量控制和性能优化,需要从前端(通过JavaScript实现倒计时、避免重复提交和限制频繁刷新)到后台各个环节的配合。流量控制主要是限制只有少部分流量进入服务后端(毕竟最终只有少部分用户能够秒杀成功),同时在物理架构上使用缓存(一方面是因为读操作多写操作少;另外可以将库存放在Redis中,利用DECR原语实现减库存;同时也可以利用Redis来进行限流,道理跟限制频繁发送手机验证码是一样的)和消息队列(消息队列最为重要的作用就是“削峰”和“上下游节点解耦合”)来进行优化;此外还要采用无状态服务设计,这样才便于进行水平扩展(通过增加设备来为系统扩容)。
2. 超卖现象:比如某商品的库存为1,此时用户1和用户2并发购买该商品,用户1提交订单后该商品的库存被修改为0,而此时用户2并不知道的情况下提交订单,该商品的库存再次被修改为-1这就是超卖现象。解决超卖现象有三种常见的思路:
- 悲观锁控制:查询商品数量的时候就用`select ... for update`对数据加锁,这样的话用户1查询库存时,用户2因无法读取库存数量被阻塞,直到用户1提交或者回滚了更新库存的操作后才能继续,从而解决了超卖问题。但是这种做法对并发访问量很高的商品来说性能太过糟糕,实际开发中可以在库存小于某个值时才考虑加锁,但是总的来说这种做法不太可取。
- 乐观锁控制:查询商品数量不用加锁,更新库存的时候设定商品数量必须与之前查询数量相同才能更新,否则说明其他事务已经更新了库存,必须重新发出请求。这种做法要求事务隔离级别为可重复读,否则仍然会产生问题。
- 尝试减库存:将上面的查询(`select`)和更新(`update`)操作合并为一条SQL操作,更新库存的时候,在`where`筛选条件中加上`库存>=购买数量``库存-购买数量>=0`的条件。
> 提示:有兴趣的可以自己在知乎上看看关于这类问题的讨论。
### 静态资源管理
静态资源的管理可以自己架设文件服务器或者分布式文件服务器(FastDFS),但是一般的项目中没有必要这样做而且效果未必是最好的,我们建议使用云存储服务来管理网站的静态资源,国内外的云服务提供商如亚马逊、阿里云、腾讯云、七牛、LeanCloud、Bmob等都提供了非常优质的云存储服务,而且价格也是一般公司可以接受的。可以参考《在阿里云OSS上托管静态网站》一文来完成对网站静态资源的管理,代码相关的内容可以参考阿里云的[对象存储 OSS开发人员指南](https://www.alibabacloud.com/zh/support/developer-resources)
### 全文检索
#### 方案选择
1. 使用数据库的模糊查询功能 - 效率低,每次需要全表扫描,不支持分词。
2. 使用数据库的全文检索功能 - MySQL 5.6以前只适用于MyISAM引擎,检索操作和其他的DML操作耦合在数据库中,可能导致检索操作非常缓慢,数据量达到百万级性能显著下降,查询时间很长。
3. 使用开源搜索引擎 - 索引数据和原始数据分离,可以使用ElasticSearch或Solr来提供外置索引服务,如果不考虑高并发的全文检索需求,纯Python的Whoosh也可以考虑。ElasticSearch-Analysis-IK
#### ElasticSearch
ElasticSearch是一个高可扩展的开源全文搜索和分析引擎,它允许存储、搜索和分析大量的数据,并且这个过程是近实时的。它通常被用作底层引擎和技术,为复杂的搜索功能和要求提供动力,大家熟知的维基百科、Stack-Overflow、Github都使用了ElasticSearch。
ElasticSearch的底层是开源搜索引擎[Lucene](https://lucene.apache.org/)。但是直接用Lucene会非常麻烦,必须自己编写代码去调用它的接口而且只支持Java语言。ElasticSearch相当于是对Lucene进行了封装,提供了REST API的操作接口。搜索引擎在对数据构建索引时,需要进行分词处理。ElasticSearch不支持基于中文分词的索引,需要配合扩展elasticsearch-analysis-ik来实现中文分词处理。
ElasticSearch的安装和配置可以参考[《ElasticSearch之Docker安装》](https://blog.csdn.net/jinyidong/article/details/80475320)。除了ElasticSearch之外,也可以使用Solr、Whoosh等来提供搜索引擎服务,基本上Django项目中可以考虑如下两套方案:
- Haystack(django-haystack / drf-haystack) + Whoosh + Jieba
- Haystack (django-haystack / drf-haystack)+ ElasticSearch + ElasticSearch-Analysis-IK
#### Django对接ElasticSearch
Python对接ElasticSearch的第三方库是HayStack,在Django项目中可以使用django-haystack,通过HayStack可以在不修改代码对接多种搜索引擎服务。
```shell
pip install django-haystack elasticsearch
```
配置文件:
```Python
INSTALLED_APPS = [
...
'haystack',
...
]
HAYSTACK_CONNECTIONS = {
'default': {
# 引擎配置
'ENGINE':
'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
# 搜索引擎服务的URL
'URL': 'http://...',
# 索引库的名称
'INDEX_NAME': '...',
},
}
# 添加/删除/更新数据时自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
```
索引类:
```Python
from haystack import indexes
class HouseInfo(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
return HouseInfo
def index_queryset(self, using=None):
return self.get_model().objects.all()
```
编辑text字段的模板(需要放在templates/search/indexes/rent/houseinfo_text.txt):
```
{{object.title}}
{{object.detail}}
```
配置URL:
```Python
urlpatterns = [
# ...
url('search/', include('haystack.urls')),
]
```
生成初始索引:
```Shell
python manage.py rebuild_index
```
> 说明:可以参考[《Django Haystack 全文检索与关键词高亮》](https://www.zmrenwu.com/post/45/)一文来更深入的了解基于Haystack的全文检索操作。
\ No newline at end of file
......@@ -61,32 +61,33 @@ API接口返回的数据通常都是JSON或XML格式,我们这里不讨论后
请求参数:
| 参数名 | 类型 | 是否必填 | 参数位置 | 说明 |
| ------ | ------ | -------- | -------- | ------------------------------------ | |
| ------ | ------ | -------- | -------- | ------------------------------------ |
| page | 整数 | 否 | 查询参数 | 页码,默认值1 |
| size | 整数 | 否 | 查询参数 | 每次获取评论数量(10~100),默认值20 |
| key | 字符串 | 是 | 请求头 | 用户的身份标识 |
响应信息:
```JSON
{
"code": 10000,
"message": "获取评论成功",
"page": 1,
"size": 10,
"totalPage": 35,
"contents": [
{
"userId": 1700095,
"nickname": "王大锤",
"pubDate": "2018年7月31日",
"content": "小编是不是有病呀"
"content": "小编是不是有病呀",
/* ... */
},
{
"userId", 1995322,
"nickname": "白元芳",
"pubDate": "2018年8月2日",
"content": "楼上说得好"
"content": "楼上说得好",
/* ... */
}
]
......@@ -108,16 +109,15 @@ API接口返回的数据通常都是JSON或XML格式,我们这里不讨论后
请求参数:
| 参数名 | 类型 | 是否必填 | 参数位置 | 说明 |
| ------- | ------ | -------- | -------- | ------------------------------------ |
| userId | 字符串 | 是 | 消息体 | 用户ID |
| token | 字符串 | 是 | 消息体 | 用户的令牌或URL的签名 |
| content | 字符串 | 是 | 消息体 | 评论的内容 |
| 参数名 | 类型 | 是否必填 | 参数位置 | 说明 |
| ------- | ------ | -------- | -------- | ---------- |
| userId | 字符串 | 是 | 消息体 | 用户ID |
| key | 字符串 | 是 | 请求头 | 用户的令牌 |
| content | 字符串 | 是 | 消息体 | 评论的内容 |
响应信息:
```JSON
{
"code": 10001,
"message": "创建评论成功",
......@@ -129,5 +129,3 @@ API接口返回的数据通常都是JSON或XML格式,我们这里不讨论后
/* ... */
}
```
\ No newline at end of file
## 英语面试
以下用I表示面试官(Interviewer),用C表示面试者(Candidate)。
### 开场寒暄
1. I: Thanks for waiting. (Please follow me.)
C: It's no problem.
2. I: How are you doing this morning?
C: I'm great. / I'm doing fine. Thank you. / How about you?
3. I: How did you get here?
C: I took the subway here. / I drove here.
4. I: Glad to meet you.
C: Glad to meet you. / It's great to finally meet you in person. (之前电话沟通过的)
### 正式面试
#### 人力面试
1. I: Can you tell me a little bit about yourself? (介绍下自己)
原则:不要谈私生活和奇怪的癖好(英雄联盟干到钻石),因为别人更想知道的是你的专业技能(qulifications)和工作经验(experience),所以重点在你之前的公司(company name)、职位(title)、时间(years)和主要职责(major responsibilities)
C: Thank you for having me. My name is Dachui WANG. I'm 25 years old, and I'm single. I have a Bachelor's Degree of Computer Science from Tsinghua University. I was a Junior Java Programmer for ABC Technologies during my college life. Then I become an intermediate Java engineer for XYZ Corporation in last two years. Programming is my everyday life and programming is where my passion is. I think I have a good knowledge of Java enterprise application developement using light-weight frameworks like Spring, Guice, Hibernate and other open source middle-ware like Dubbo, Mycat, rocketmq and so on and so forth. I love reading, travelling and playing basketball in my spare time. That's all! Thank you!
2. I: How would you describe your personality? (你的性格)
C: I'm hard working, eager to learn, and very serious about my work. I enjoy working with other people and I love challenges.
3. I: What do you know about our company? (你对我们公司有什么了解)
(需要做功课,了解公司的状况和企业文化,该公司在这个行业中的一个状况,有什么核心业务,主要的竞争对手有哪些)
C: The one thing that I like the most about our company is your core values. I think they're very important in this industry because …(自由发挥的部分)... I personally really believe in the cause as well. Of course, I'm very interested in your products such as …(功课部分)… and the techniques behind them.
4. I: Why are you leaving your last job? (为什么离职)
C: I want to advance my career and I think this job offers more challenges and opportunities for me do to that.
5. I: What do you see yourself in 3 or 5 years? (3-5年职业规划)
C: My long term goals involve growing with the company, where I can continue to learn, to take on additional responsibilities and to contribute as much value as I can. I intend to take advantage of all of these.
6. I: What's your salary expectation? (期望薪资)
C: My salary expectation is in line with my experience and qualifications. I believe our company will pay me and every other employee fairly. (把球踢给对方先看看对方报价是多少,如果对方非要你报价再说后面的内容) I think 15 thousands RMB or above is fitting for me to leave in Chengdu.
7. I: Do you have any questions for me? (问面试官的问题)
C: What's the growth potential for this position?
#### 技术面试
1. I: What's difference between an interface and an abstract class?
2. I: What are pass by reference and pass by value?
3. I: What's the difference between process and threads?
4. I: Explain the available thread state in high-level.
5. I: What's deadlocks? How to avoid them?
6. I: How HashMap works in Java?
7. I: What's the difference between ArrayList and LinkedList? (类似的问题还有很多,比如比较HashSet和TreeSet、HashMap和Hashtable)
8. I: Tell me what you know about garbage collection in Java.
9. I: What're two types of exceptions in Java?
10. I: What's the advantage of PreparedStatement over Statement?
11. I: What's the use of CallableStatement?
12. I: What does connection pool mean?
13. I: Explain the life cycle of a Servlet.
14. I: What's the difference between redirect and forward?
15. I: What's EL? What're implicit objects of EL?
16. I: Tell me what you know about Spring framework and its benefits.
17. I: What're different types of dependency injection.
18. I: Are singleton beans thread safe in Spring framework?
19. I: What're the benefits of Spring framework's transaction management?
20. I: Explain what's AOP.
21. I: What's a proxy and how to implement proxy pattern?
22. I: How Spring MVC works?
23. I: What's the working scenario of Hibernate and MyBatis?
24. I: How to implement SOA?
25. I: Make a brief introduction of the projects you are involved before?
上面主要是面试Java程序员的问题,但是整个流程大致如此。
## 面试中的公共问题
### 计算机基础
1. TCP/IP模型相关问题。
> 建议阅读阮一峰的[《互联网协议入门(一)》](http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)和[《互联网协议入门(二)》](http://www.ruanyifeng.com/blog/2012/06/internet_protocol_suite_part_ii.html)。
2. HTTP和HTTPS相关问题。
> 建议阅读阮一峰的[《HTTP 协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)和[《SSL/TLS协议运行机制的概述》](http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html)。
3. Linux常用命令和服务。
4. 进程和线程之间的关系。什么时候用多线程?什么时候用多进程?。
5. 关系型数据库相关问题(ACID、事务隔离级别、锁、SQL优化)。
6. 非关系型数据库相关问题(CAP/BASE、应用场景)。
### Python基础
1. 开发中用过哪些标准库和三方库。
> 标准库:sys / os / re / math / random / logging / json / pickle / shelve / socket / datetime / hashlib / configparser / urllib / itertools / collections / functools / threading / multiprocess / timeit / atexit / abc / asyncio / base64 / concurrent.futures / copy / csv / operator / enum / heapq / http / profile / pstats / ssl / unitest / uuid
2. 装饰器的作用、原理和实现。
3. 使用过哪些魔法方法。
> 建议阅读[《Python魔术方法指南》](https://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/a-guide-to-pythons-magic-methods.html)。
4. 生成式、生成器、迭代器的编写。
5. 列表、集合、字典的底层实现。
6. 垃圾回收相关问题。
7. 并发编程的相关问题。
8. 协程和异步I/O相关知识。
### Django和Flask
1. MVC架构(MTV)解决了什么问题。
2. 中间件的执行流程以及如何自定义中间件。
3. REST数据接口如何设计(URL、域名、版本、过滤、状态码、安全性)。
> 建议阅读阮一峰的[《RESTful API设计指南》](http://www.ruanyifeng.com/blog/2014/05/restful_api.html)。
4. 使用ORM框架实现CRUD操作的相关问题。
- 如何实现多条件组合查询 / 如何执行原生的SQL / 如何避免N+1查询问题
5. 如何执行异步任务和定时任务。
6. 如何实现页面缓存和查询缓存?缓存如何预热?
### 爬虫相关
1. Scrapy框架的组件和数据处理流程。
2. 爬取的目的(项目中哪些地方需要用到爬虫的数据)。
3. 使用的工具(抓包、下载、清理、存储、分析、可视化)。
4. 数据的来源(能够轻松的列举出10个网站)。
5. 数据的构成(抓取的某个字段在项目中有什么用)。
6. 反反爬措施(限速、请求头、Cookie池、代理池、Selenium、PhantomJS、RoboBrowser、TOR、OCR)。
7. 数据的体量(最后抓取了多少数据,多少W条数据或多少个G的数据)。
8. 后期数据处理(持久化、数据补全、归一化、格式化、转存、分类)。
### 数据分析
1. 科学运算函数库(SciPy和NumPy常用运算)。
2. 数据分析库(Pandas中封装的常用算法)。
3. 常用的模型及对应的场景(分类、回归、聚类)。
4. 提取了哪些具体的指标。
5. 如何评价模型的优劣。
6. 每种模型实际操作的步骤,对结果如何评价。
### 项目相关
1. 项目团队构成以及自己在团队中扮演的角色(在项目中的职责)。
2. 项目的业务架构(哪些模块及子模块)和技术架构(移动端、PC端、后端技术栈)。
3. 软件控制管理相关工具(版本控制、问题管理、持续集成)。
4. 核心业务实体及其属性,实体与实体之间的关系。
5. 用到哪些依赖库,依赖库主要解决哪方面的问题。
6. 项目如何部署上线以及项目的物理架构(Nginx、Gunicorn/uWSGI、Redis、MongoDB、MySQL、Supervisor等)。
7. 如何对项目进行测试,有没有做过性能调优。
8. 项目中遇到的困难有哪些,如何解决的。
\ No newline at end of file
## 项目部署上线指南
### 更新Python环境到3.x
### 准备上线
```Shell
yum -y install gcc zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel
wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz
xz -d Python-3.7.0.tar.xz
tar -xvf Python-3.7.0.tar
cd Python-3.7.0
./configure --prefix=/usr/local/python3 --enable-optimizations
make && make install
cd ~
vim .bash_profile
export PATH=$PATH:/usr/local/python3/bin
ln -s /usr/local/python3/bin/python3 /usr/bin/python3
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3
```
1. 上线前的检查工作。
```Shell
python manage.py check --deploy
```
2. 将DEBUG设置为False并配置ALLOWED_HOSTS。
```Python
DEBUG = False
ALLOWED_HOSTS = ['*']
```
3. 安全相关的配置。
```Python
# 保持HTTPS连接的时间
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# 自动重定向到安全连接
SECURE_SSL_REDIRECT = True
# 避免浏览器自作聪明推断内容类型
SECURE_CONTENT_TYPE_NOSNIFF = True
# 避免跨站脚本攻击
SECURE_BROWSER_XSS_FILTER = True
# COOKIE只能通过HTTPS进行传输
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# 防止点击劫持攻击手段 - 修改HTTP协议响应头
# 当前网站是不允许使用<iframe>标签进行加载的
X_FRAME_OPTIONS = 'DENY'
```
4. 敏感信息放到环境变量或文件中。
```Python
SECRET_KEY = os.environ['SECRET_KEY']
DB_USER = os.environ['DB_USER']
DB_PASS = os.environ['DB_PASS']
REDIS_AUTH = os.environ['REDIS_AUTH']
```
### 更新服务器Python环境到3.x
1. 安装底层依赖库。
```Shell
yum -y install wget gcc zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel
```
2. 下载Python源代码。
```Shell
wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz
```
3. 解压缩和解归档。
```Shell
xz -d Python-3.7.0.tar.xz
tar -xvf Python-3.7.0.tar
```
4. 执行配置生成Makefile(构建文件)。
```Shell
cd Python-3.7.0
./configure --prefix=/usr/local/python37 --enable-optimizations
```
5. 构建和安装。
```Shell
make && make install
```
6. 配置PATH环境变量并激活。
```Shell
cd ~
vim .bash_profile
```
```INI
... 此处省略上面的代码...
export PATH=$PATH:/usr/local/python37/bin
... 此处省略下面的代码...
```
```Shell
source .bash_profile
```
7. 注册软链接(符号链接)。
```Shell
ln -s /usr/local/python37/bin/python3.7 /usr/bin/python3
ln -s /usr/local/python37/bin/pip3.7 /usr/bin/pip3
```
8. 测试Python环境是否更新成功。
```Shell
python3 --version
python --version
```
### 项目目录结构
下面是项目的目录结构,四个文件夹`conf``logs``src``venv`分别用来保存项目的配置文件、日志文件、源代码和虚拟环境。`conf`目录下的子目录`cert`中保存了配置HTTPS需要使用的证书和密钥
假设项目文件夹为`project`,下面的四个子目录分别是:`conf`、`logs`、`src`和`venv`分别用来保存项目的配置文件、日志文件、源代码和虚拟环境。其中,`conf`目录下的子目录`cert`中保存了配置HTTPS需要使用的证书和密钥;`src`目录下的项目代码可以通过版本控制工具从代码仓库中检出;虚拟环境可以通过venv或其他工具进行创建
```
project
......@@ -35,13 +137,14 @@ project
│   └── uwsgi.log
├── requirements.txt
├── src
│   └── fang
│   └── fangall
│   ├── api
│   ├── common
│   ├── fang
│   ├── forum
│   ├── rent
│   ├── user
│   ├── manage.py
│   ├── README.md
│   ├── rent
│   ├── static
│   └── templates
│  
......@@ -84,129 +187,188 @@ project
└── pyvenv.cfg
```
下面以阿里云为例,简单说明如何为项目注册域名、解析域名以及购买权威机构颁发的证书。
1. [注册域名](https://wanwang.aliyun.com/domain/)。
![](./res/aliyun-domain.png)
2. [域名备案](https://beian.aliyun.com/)。
![](./res/aliyun-keeprecord.png)
3. [域名解析](https://dns.console.aliyun.com/#/dns/domainList)。
![](./res/aliyun-dnslist.png)
![](./res/aliyun-resolve-settings.png)
4. [购买证书](https://www.aliyun.com/product/cas)。
![](./res/aliyun-certificate.png)
### uWSGI的配置
可以激活项目的虚拟环境并通过pip安装uWSGI
1. 在`project`目录下创建并激活虚拟环境
```Shell
pip install uwsgi
```
```Shell
python3 -m venv venv
source venv/bin/activate
```
`/root/project/conf/uwsgi.ini`
```INI
[uwsgi]
# 配置前导路径
base=/root/project
# 配置项目名称
name=fang
# 守护进程
master=true
# 进程个数
processes=4
# 虚拟环境
pythonhome=%(base)/venv
# 项目地址
chdir=%(base)/src/%(name)
# 指定python解释器
pythonpath=%(pythonhome)/bin/python
# 指定uwsgi文件
module=%(name).wsgi
# 通信的地址和端口(自己服务器的IP地址和端口)
socket=172.18.61.250:8000
# 日志文件地址
logto = %(base)/logs/uwsgi.log
```
2. 安装项目依赖项。
可以先将“通信的地址和端口”项等号前面改为http来进行测试,如果没有问题再改回成socket,然后通过Nginx来实现项目的“动静分离”(静态资源交给Nginx处理,动态内容交给uWSGI处理)。
```Shell
pip install -r requirements.txt
```
```Shell
uwsgi --ini uwsgi.ini &
```
3. 通过pip安装uWSGI。
### Nginx的配置
#### 全局配置
`/etc/nginx/nginx.conf`
```Shell
pip install uwsgi
```
```Nginx
# 全局配置
# 用户(可以设置为)
user root;
# 工作进程数(建议跟CPU的核数量一致)
worker_processes auto;
# 错误日志
error_log /var/log/nginx/error.log;
# 进程文件
pid /run/nginx.pid;
4. 修改uWSGI的配置文件(`/root/project/conf/uwsgi.ini`)。
```INI
[uwsgi]
# 配置前导路径
base=/root/project
# 配置项目名称
name=fangall
# 守护进程
master=true
# 进程个数
processes=4
# 虚拟环境
pythonhome=%(base)/venv
# 项目地址
chdir=%(base)/src/%(name)
# 指定python解释器
pythonpath=%(pythonhome)/bin/python
# 指定uwsgi文件
module=%(name).wsgi
# 通信的地址和端口(自己服务器的IP地址和端口)
socket=172.18.61.250:8000
# 日志文件地址
logto = %(base)/logs/uwsgi.log
```
# 包含其他的配置
include /usr/share/nginx/modules/*.conf;
> 说明:可以先将“通信的地址和端口”项等号前面改为http来进行测试,如果没有问题再改回 成socket,然后通过Nginx来实现项目的“动静分离”(静态资源交给Nginx处理,动态内容交给 uWSGI处理)。按照下面的方式可以启动uWSGI服务器。
# 工作模式和连接上限
events {
use epoll;
worker_connections 1024;
}
5. 启动服务器。
# HTTP服务器相关配置
http {
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志
access_log /var/log/nginx/access.log main;
# 开启高效文件传输模式
sendfile on;
# 用sendfile传输文件时有利于改善性能
tcp_nopush on;
# 禁用Nagle来解决交互性问题
tcp_nodelay on;
# 客户端保持连接时间
keepalive_timeout 15;
types_hash_max_size 2048;
# 包含MIME类型的配置
include /etc/nginx/mime.types;
# 默认使用二进制流格式
default_type application/octet-stream;
# 包含其他配置文件
include /etc/nginx/conf.d/*.conf;
# 包含项目的Nginx配置文件
include /root/project/conf/*.conf;
}
```
```Shell
uwsgi --ini uwsgi.ini &
```
### Nginx的配置
#### 局部配置
1. 修改全局配置文件(`/etc/nginx/nginx.conf`)。
```Nginx
# 配置用户
user root;
# 工作进程数(建议跟CPU的核数量一致)
worker_processes auto;
# 错误日志
error_log /var/log/nginx/error.log;
# 进程文件
pid /run/nginx.pid;
# 包含其他的配置
include /usr/share/nginx/modules/*.conf;
# 工作模式和连接上限
events {
use epoll;
worker_connections 1024;
}
# HTTP服务器相关配置
http {
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志
access_log /var/log/nginx/access.log main;
# 开启高效文件传输模式
sendfile on;
# 用sendfile传输文件时有利于改善性能
tcp_nopush on;
# 禁用Nagle来解决交互性问题
tcp_nodelay on;
# 客户端保持连接时间
keepalive_timeout 15;
types_hash_max_size 2048;
# 包含MIME类型的配置
include /etc/nginx/mime.types;
# 默认使用二进制流格式
default_type application/octet-stream;
# 包含其他配置文件
include /etc/nginx/conf.d/*.conf;
# 包含项目的Nginx配置文件
include /root/project/conf/*.conf;
}
```
2. 编辑局部配置文件(`/root/project/conf/nginx.conf`)。
```Nginx
server {
listen 80;
server_name _;
access_log /root/project/logs/access.log;
error_log /root/project/logs/error.log;
location / {
include uwsgi_params;
uwsgi_pass 172.18.61.250:8000;
}
location /static/ {
alias /root/project/src/fangall/static/;
expires 30d;
}
}
server {
listen 443;
server_name _;
ssl on;
access_log /root/project/logs/access.log;
error_log /root/project/logs/error.log;
ssl_certificate /root/project/conf/cert/214915882850706.pem;
ssl_certificate_key /root/project/conf/cert/214915882850706.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
location / {
include uwsgi_params;
uwsgi_pass 172.18.61.250:8000;
}
location /static/ {
alias /root/project/src/fangall/static/;
expires 30d;
}
}
```
`/root/project/conf/nginx.conf`
到此为止,我们可以启动Nginx来访问我们的应用程序,HTTP和HTTPS都是没有问题的,如果Nginx已经运行,在修改配置文件后,我们可以用下面的命令重新启动Nginx。
```Nginx
server {
listen 80;
server_name _;
access_log /root/project/logs/access.log;
error_log /root/project/logs/error.log;
location / {
include uwsgi_params;
uwsgi_pass 172.18.61.250:8000;
}
location /static/ {
alias /root/project/src/fang/static/;
expires 30d;
}
}
```
3. 重启Nginx服务器。
到此为止,我们可以启动Nginx来访问我们的应用程序,HTTP和HTTPS都是没有问题的,如果Nginx已经运行,在修改配置文件后,我们可以用下面的命令重新启动Nginx。
```Shell
nginx -s reload
```
```Shell
nginx -s reload
```
> 说明:可以对Django项目使用`python manage.py collectstatic`命令将静态资源收集到指定目录下,要做到这点只需要在项目的配置文件`settings.py`中添加`STATIC_ROOT`配置即可。
#### 负载均衡配置
下面的配置中我们使用Nginx为HTTP、HTTPS以及Redis配置负载均衡。
下面的配置中我们使用Nginx实现负载均衡,为另外的三个Nginx服务器(通过Docker创建)提供反向代理服务。
```Shell
docker run -d -p 801:80 --name nginx1 nginx:latest
docker run -d -p 802:80 --name nginx2 nginx:latest
docker run -d -p 803:80 --name nginx3 nginx:latest
```
```Nginx
user root;
......@@ -220,24 +382,12 @@ events {
worker_connections 1024;
}
stream {
upstream redis.local {
server 172.18.61.250:36379;
server 172.18.61.250:46379;
server 172.18.61.250:56379;
}
server {
listen 6379;
proxy_pass redis.local;
}
}
# 为HTTP服务配置负载均衡
http {
upstream fang.com {
server 172.18.61.250:801;
server 172.18.61.250:802;
server 172.18.61.250:803;
server 172.18.61.250:801 weight=4;
server 172.18.61.250:802 weight=2;
server 172.18.61.250:803 weight=2;
}
server {
......@@ -267,12 +417,193 @@ http {
}
```
> 说明:上面的配置文件中的Nginx服务器(3个节点)和Redis服务器(2个节点,每个节点是1个master和2个slave的配置)都是通过Docker来创建的,实际部署的时候无论是否使用Docker进行部署,这些主机应该都是独立的服务器
> 说明:Nginx在配置负载均衡时,默认使用WRR(加权轮询算法),除此之外还支持ip_hash、fair(需要安装upstream_fair模块)和url_hash算法。此外,在配置upstream模块时可以指定服务器的状态值,包括:backup(备份机器,其他服务器不可用时才将请求分配到该机器)、down、fail_timeout(请求失败达到max_fails后的暂停服务时间)、max_fails(允许请求失败的次数)和weight(轮询的权重)
### Keepalived
当使用Nginx进行负载均衡配置时,要考虑负载均衡服务器宕机的情况。为此可以使用Keepalived来实现负载均衡主机和备机的热切换,从而保证系统的高可用性。Keepalived的配置还是比较复杂,通常由专门做运维的人进行配置,一个基本的配置可以参照[《Keepalived的配置和使用》](https://www.jianshu.com/p/dd93bc6d45f5)。
### MySQL主从复制
下面还是基于Docker来演示如何配置MySQL主从复制。我们事先准备好MySQL的配置文件以及保存MySQL数据和运行日志的目录,然后通过Docker的数据卷映射来指定容器的配置、数据和日志文件的位置。
```Shell
root
└── mysql
├── conf
│   ├── master
│   │   └── mysqld.cnf
│   ├── slave1
│   │   └── mysqld.cnf
│   ├── slave2
│   │   └── mysqld.cnf
│   └── slave3
│   └── mysqld.cnf
└── data
├── master
├── slave1
├── slave2
└── slave3
```
1. MySQL的配置文件(master和slave的配置文件需要不同的server-id)。
```
[mysqld]
pid-file=/var/run/mysqld/mysqld.pid
socket=/var/run/mysqld/mysqld.sock
datadir=/var/lib/mysql
log-error=/var/log/mysql/error.log
server-id=1
log_bin=/var/log/mysql/mysql-bin.log
expire_logs_days=30
max_binlog_size=256M
symbolic-links=0
```
2. 创建和配置master。
```Shell
docker run -d -p 3306:3306 --name mysql57 \
-v /root/mysql/conf/master:/etc/mysql/mysql.conf.d \
-v /root/mysql/data/master:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
docker exec -it mysql57 /bin/bash
```
```Shell
mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 1
Server version: 5.7.23-log MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> grant replication slave on *.* to 'slave'@'%' identified by 'iamslave';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000001 | 590 | | | |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
mysql> quit
Bye
exit
```
上面创建Docker容器时使用的`-v`参数(`--volume`)表示映射数据卷,冒号前是宿主机的目录,冒号后是容器中的目录,这样相当于将宿主机中的目录挂载到了容器中。
3. 创建和配置slave。
```Shell
docker run -d -p 3307:3306 --name mysql57-slave-1 \
-v /root/mysql/conf/slave1:/etc/mysql/mysql.conf.d \
-v /root/mysql/data/slave1:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
--link mysql57:mysql57 mysql:5.7
docker exec -it mysql57-slave-1 /bin/bash
```
```Shell
mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.23-log MySQL Community Server (GPL)
Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> reset slave;
Query OK, 0 rows affected (0.02 sec)
mysql> change master to master_host='mysql57', master_user='slave', master_password='iamslave', master_log_file='mysql-bin.000003', master_log_pos=590;
Query OK, 0 rows affected, 2 warnings (0.03 sec)
mysql> start slave;
Query OK, 0 rows affected (0.01 sec)
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: mysql57
Master_User: slave
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 590
Relay_Log_File: f352f05eb9d0-relay-bin.000002
Relay_Log_Pos: 320
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 590
Relay_Log_Space: 534
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
Master_UUID: 30c38043-ada1-11e8-8fa1-0242ac110002
Master_Info_File: /var/lib/mysql/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
1 row in set (0.00 sec)
mysql> quit
Bye
exit
```
接下来可以如法炮制配置出slave2和slave3,这样就可以搭建起一个“一主带三从”的主从复制环境。上面创建创建容器时使用的`--link`参数用来配置容器在网络上的主机名(网络地址别名),下一节有这个知识点的介绍。
### Docker
事实上,项目上线中最为麻烦的事情就是配置软件运行环境,环境的差异会给软件的安装和部署带来诸多的麻烦,而Docker正好可以解决这个问题。关于Docker在之前的文档中我们已经介绍过了,接下来我们对Docker的知识做一些必要的补充。
......@@ -334,6 +665,81 @@ http {
```Shell
docker run --link <container-name>:<alias-name>
```
我们在Docker中完成项目的部署,并且将整个部署好的容器打包成镜像文件进行分发和安装,这样就可以解决项目在多个节点上进行部署时可能遇到的麻烦。
\ No newline at end of file
如果我们能够在Docker中完成项目的部署,并且将整个部署好的容器打包成镜像文件进行分发和安装,这样就可以解决项目在多个节点上进行部署时可能遇到的麻烦,而且整个部署可以在很短的时间内完成。
### Supervisor
[Supervisor](https://github.com/Supervisor/supervisor)是一个用Python写的进程管理工具,可以很方便的用来在类Unix系统下启动、重启(自动重启程序)和关闭进程。
1. 安装Supervisor。
```Shell
yum -y install supervisor
```
2. 查看Supervisor的配置文件。
```Shell
vim /etc/supervisord.conf
...
[include]
files = supervisord.d/*.ini
```
可以看出自定义的管理配置代码可以放在`/etc/supervisord.d`目录中,并且文件名以`ini`作为后缀即可。
3. 编写管理配置代码。
```Shell
cd /etc/supervisord.d
vim fang.ini
```
```INI
[program:fang]
...
```
4. 启动Supervisor服务和查看状态。
```Shell
systemctl start supervisord
supervisorctl status
```
### 其他服务
1. 常用开源软件。
| 功能 | 开源方案 |
| ------------------- | ------------------------- |
| 版本控制工具 | Git、Mercurial、SVN |
| 缺陷管理 | Redmine、Mantis |
| 负载均衡 | Nginx、LVS、HAProxy |
| 邮件服务 | Postfix、Sendmail |
| HTTP服务 | Nginx、Apache |
| 消息队列 | RabbitMQ、ZeroMQ、Redis |
| 文件系统 | FastDFS |
| 基于位置服务(LBS) | MongoDB、Redis |
| 监控服务 | Nagios、Zabbix |
| 关系型数据库 | MySQL、PostgreSQL |
| 非关系型数据库 | MongoDB、Redis、Cassandra |
| 搜索引擎 | ElasticSearch、Solr |
| 缓存服务 | Mamcached、Redis |
2. 常用云服务。
| 功能 | 可用的云服务 |
| -------------- | --------------------------------------- |
| 团队协作工具 | Teambition、钉钉 |
| 代码托管平台 | Github、Gitee、CODING |
| 邮件服务 | SendCloud |
| 云存储(CDN) | 七牛、OSS、LeanCloud、Bmob、又拍云、AWS |
| 移动端推送 | 极光、友盟、百度 |
| 即时通信 | 环信、融云 |
| 短信服务 | 云片、极光、Luosimao、又拍云 |
| 第三方登录 | 友盟、ShareSDK |
| 网站监控和统计 | 阿里云监控、监控宝、百度云观测、小鸟云 |
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment