带你进入异步Django+Vue的世界 - Didi打车实战(2) https://www.jianshu.com/p/f6a83315e055
Vue + Vuetify 前端鉴权实现
Demo: https://didi-taxi.herokuapp.com/
后台数据模型设计
数据模型是后台的灵魂,需要考虑周全。
数据模型的更新,使用python manage.py makemigrations
可以很方便地迁移
- User,继承
AbstractUser
group
指明用户是乘客还是司机
photo
用来上存储用户的头像
# /backend/api/models.py
from django.db import models
from django.conf import settings
from django.shortcuts import reverse
from django.contrib.auth.models import AbstractUser
import uuid
class User(AbstractUser):
photo = models.ImageField(upload_to='photos', null=True, blank=True)
@property
def group(self):
groups = self.groups.all()
return groups[0].name if groups else None
- Trip,继承通用模型
Model
id
用uuid4
来指明一下唯一的订单编号
pick_up_address
/drop_off_address
指明上车地点和目的地
status
用来存储订单的状态:
- 下单REQUESTED
- 已接单STARTED
- 行程中IN_PROGRESS
- 行程结束COMPLETED
driver
/rider
是外键,关联User
模型
# /backend/api/models.py
class Trip(models.Model):
REQUESTED = 'REQUESTED'
STARTED = 'STARTED'
IN_PROGRESS = 'IN_PROGRESS'
COMPLETED = 'COMPLETED'
STATUSES = (
(REQUESTED, REQUESTED),
(STARTED, STARTED),
(IN_PROGRESS, IN_PROGRESS),
(COMPLETED, COMPLETED),
)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
pick_up_address = models.CharField(max_length=255)
drop_off_address = models.CharField(max_length=255)
status = models.CharField(max_length=20, choices=STATUSES, default=REQUESTED)
driver = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.DO_NOTHING,
related_name='trip_as_driver'
)
rider = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.DO_NOTHING,
related_name='trip_as_rider'
)
def __str__(self):
return f'{self.id}'
def get_absolute_url(self):
return reverse('trip:trip_detail', kwargs={'trip_id': self.id})
- 把模型登记到django admin里:
# /backend/api/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
from .models import User, Trip
@admin.register(User)
class UserAdmin(DefaultUserAdmin):
...
@admin.register(Trip)
class TripAdmin(admin.ModelAdmin):
fields = (
'id', 'pick_up_address', 'drop_off_address', 'status',
'driver', 'rider', 'created', 'updated',
)
list_display = (
'id', 'pick_up_address', 'drop_off_address', 'status',
'driver', 'rider', 'created', 'updated',
)
list_filter = ('status',)
readonly_fields = (
'id', 'created', 'updated',
)
- DRF只处理鉴权和trips view,所以先删除不需要的URL:
# /backend/urls.py 删除后如下所示:
from django.contrib import admin
from django.urls import path, re_path, include
from .api.views import index_view, serve_worker_view
urlpatterns = [
# http://localhost:8000/
path('', index_view, name='index'),
# serve static files for PWA
path('index.html', index_view, name='index'),
re_path(r'^(?Pmanifest).json$', serve_worker_view, name='manifest'),
re_path(r'^(?P[-\w\d.]+).js$', serve_worker_view, name='serve_worker'),
re_path(r'^(?Probots).txt$', serve_worker_view, name='robots'),
# http://localhost:8000/admin/
path('admin/', admin.site.urls),
# support vue-router history mode
re_path(r'^\S+$', index_view, name='SPA_reload'),
]
删除不需要的view:
# /backend/api/views.py
删除 from .models import Message, MessageSerializer
删除 class MessageViewSet
模型更新:
(didi-project) git/didi-project$ python manage.py makemigrations
Migrations for 'api':
backend/api/migrations/0002_auto_20190518_0708.py
- Create model Trip
- Delete model Message
- Add field photo to user
- Add field driver to trip
- Add field rider to trip
(didi-project) git/didi-project$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, api, auth, contenttypes, sessions
Running migrations:
Applying api.0002_auto_20190518_0708... OK
在Admin里测试下Trip
创建几个测试用户,然后创建Trip订单:
用户查看Trip功能
- 后端需要提供Serializer、View、Url
Serializer
# trips/serializers.py
from .models import Trip
class TripSerializer(serializers.ModelSerializer):
class Meta:
model = Trip
fields = '__all__'
read_only_fields = ('id', 'created', 'updated',)
其中三个字段,是只读的,不需要Serializer创建: id
, created
, updated
.
View
Add the TripView
to api/views.py:
# trips/views.py
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.forms import AuthenticationForm
from rest_framework import generics, permissions, status, views, viewsets # new
from rest_framework.response import Response
from .models import Trip # new
from .serializers import TripSerializer, UserSerializer # new
class TripView(viewsets.ReadOnlyModelViewSet):
permission_classes = (permissions.IsAuthenticated,)
queryset = Trip.objects.all()
serializer_class = TripSerializer
TripView
非常基本,使用DRF ReadOnlyModelViewSet
:返回trip列表和 trip详情 views.
这个路由是需要鉴权的。
URLs
在总路由里,添加trips.urls子路由:
# taxi/urls.py
from django.contrib import admin
from django.urls import include, path # new
from .api.views import index_view, serve_worker_view, SignUpView, LogInView, LogOutView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/sign_up/', SignUpView.as_view(), name='sign_up'),
path('api/log_in/', LogInView.as_view(), name='log_in'),
path('api/log_out/', LogOutView.as_view(), name='log_out'),
path('api/trip/', include('api.urls', 'trip',)), # new
]
创建子路由文件:
# trips/urls.py
from django.urls import path
from .views import TripView
app_name = 'api'
urlpatterns = [
path('', TripView.as_view({'get': 'list'}), name='trip_list'),
]
更新前端,显示Trips
Home.vue里,显示所有的订单信息
# /src/views/Home.vue
当前订单
{{ card_text }}
{{ item.pick_up_address }} to {{ item.drop_off_address }}
account_circle
{{ item.status }}
created: {{ item.created }}
updated: {{ item.updated }}
Cancel
历史订单
{{ card_text }}
{{ item.pick_up_address }} to {{ item.drop_off_address }}
account_circle
{{ item.status }}
created: {{ item.created }}
updated: {{ item.updated }}
分成已完成订单和正在进行中的订单,用trip的status区别:
computed: {
...mapState(['alert', 'user']),
...mapState('messages', ['trips']),
userIsAuthenticated () {
return this.user !== null && this.$store.getters.user !== undefined
},
trips_ongoing () {
return this.trips.filter(obj => obj.status !== 'COMPLETED')
},
trips_done () {
return this.trips.filter(obj => obj.status === 'COMPLETED')
}
},
装载此页面时,读取后台的trip信息:
mounted () {
if (this.userIsAuthenticated) {
this.$store.dispatch('messages/getTrips')
}
},
Vuex store里,添加trips的操作:
# /src/store/modules/message.js
const state = {
messages: [],
trips: []
}
const mutations = {
setTrips (state, messages) {
state.trips = messages
},
const actions = {
getTrips ({ commit }) {
messageService.fetchTrips()
.then(messages => {
commit('setTrips', messages)
})
},
ajax服务:
# /src/services/messageService.js
fetchTrips () {
return api.get(`trip/`)
.then(response => response.data)
},
以上是读取所有Trips的列表,对于单条trip记录的读取,需要后台添加view:
更新views.py
-
lookup_field
告诉后台通过id来查找trip记录 -
lookup_url_kwarg
是url的是kwarg名字
# api/views.py
class TripView(viewsets.ReadOnlyModelViewSet):
lookup_field = 'id' # new
lookup_url_kwarg = 'trip_id' # new
permission_classes = (permissions.IsAuthenticated,)
queryset = Trip.objects.all()
serializer_class = TripSerializer
更新URL记录:
# api/urls.py
from django.urls import path, re_path # changed
from .views import TripView
app_name = 'api'
urlpatterns = [
path('', TripView.as_view({'get': 'list'}), name='trip_list'),
path('/', TripView.as_view({'get': 'retrieve'}), name='trip_detail'), # new
]
测试一下:
浏览器输入:http://localhost:8080/api/trip/6e446f7f-606d-488c-9274-f786b9f06800/
,应该就可以查到详情了。
用户退出时,清除Trip记录
# /src/store/modules/messages.js
signUserOut ({ commit }) {
commit('setLoading', true, { root: true })
messageService.signUserOut()
.then(messages => {
commit('setAlert', { type: 'info', msg: 'Log-out success!' }, { root: true })
commit('setUser', null, { root: true })
commit('setTrips', [])
localStorage.removeItem('user')
commit('setLoading', false, { root: true })
})
},
总结
这篇主要是数据库设计和前、后台的综合运用,加深印象。
下一篇,会进入到Django Channels + Websockets的使用。
带你进入异步Django+Vue的世界 - Didi打车实战(4)