上一篇 帶你進(jìn)入異步Django+Vue的世界 - Didi打車實(shí)戰(zhàn)(4)
Demo: https://didi-taxi.herokuapp.com/
上一篇讼积,前、后端已經(jīng)完整支持了Websockets蔫仙。
接下來,我們來實(shí)現(xiàn)創(chuàng)建訂單蝗罗、群發(fā)群收栖忠、修改訂單功能。
Refactoring: Trip返回信息
后臺返回Trip信息里设凹,driver/rider
是一個primary key舰讹,指向User。我們希望能直接看到ForeignKey: driver/rider的詳細(xì)信息闪朱。
[{created: "2019-05-20T10:08:59.950536Z"
driver: null
drop_off_address: "牛首山"
id: "4a25dde1-dd0d-422a-9e5e-706958b65046"
pick_up_address: "總統(tǒng)府"
rider: {id: 5, username: "rider3", first_name: "", last_name: "", group: "rider"}
status: "REQUESTED"
updated: "2019-05-20T10:08:59.950563Z"}, ...]
Serializer添加ReadOnlyTripSerializer月匣,關(guān)聯(lián)UserSerializer即可。
# /backend/api/serializers.py
class ReadOnlyTripSerializer(serializers.ModelSerializer):
driver = UserSerializer(read_only=True)
rider = UserSerializer(read_only=True)
class Meta:
model = Trip
fields = '__all__'
然后修改DRF view, 用戶HTTP訪問/trip/時的TripView奋姿,
#
class TripView(viewsets.ReadOnlyModelViewSet):
lookup_field = 'id'
lookup_url_kwarg = 'trip_id'
permission_classes = (permissions.IsAuthenticated,)
queryset = Trip.objects.all()
serializer_class = ReadOnlyTripSerializer # changed
Channels 創(chuàng)建訂單
當(dāng)用戶創(chuàng)建一個訂單時锄开,我們用Consumer來創(chuàng)建訂單:
- 判斷消息type是否為
create.trip
- 調(diào)用DRF
trip = serializer.create()
創(chuàng)建 - 注意Django的數(shù)據(jù)庫操作,都是同步的称诗,而Channels是異步的萍悴,所以需要加個裝飾器:
@database_sync_to_async
- 創(chuàng)建Trip記錄后,再添加用戶信息寓免,調(diào)用
ReadOnlyTripSerializer()
- 發(fā)送Websockets:
self.send_json()
- 新訂單創(chuàng)建時癣诱,通知所有的司機(jī):
channel_layer.group_send( group='drivers', message={ 'type': 'echo.message', 'data': trip_data } )
- 其中
'type': 'echo.message'
,Channels會自動調(diào)用echo_message(event)
函數(shù)袜香,保證在drivers
組里的司機(jī)們都能收到
- 其中
# api/consumers.py
from channels.db import database_sync_to_async # new
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from api.serializers import ReadOnlyTripSerializer, TripSerializer # new
class TaxiConsumer(AsyncJsonWebsocketConsumer):
# modified
async def connect(self):
user = self.scope['user']
if user.is_anonymous:
await self.close()
else:
channel_groups = []
# Add a driver to the 'drivers' group.
user_group = await self._get_user_group(self.scope['user'])
if user_group == 'driver':
channel_groups.append(self.channel_layer.group_add(
group='drivers',
channel=self.channel_name
))
# Get trips and add rider to each one's group.
self.trips = set([
str(trip_id) for trip_id in await self._get_trips(self.scope['user'])
])
for trip in self.trips:
channel_groups.append(self.channel_layer.group_add(trip, self.channel_name))
await asyncio.gather(*channel_groups)
await self.accept()
# new
async def receive_json(self, content, **kwargs):
message_type = content.get('type')
if message_type == 'create.trip':
await self.create_trip(content)
# new
async def echo_message(self, event):
await self.send_json(event)
# new
async def create_trip(self, event):
trip = await self._create_trip(event.get('data'))
trip_id = f'{trip.id}'
trip_data = ReadOnlyTripSerializer(trip).data
# Send rider requests to all drivers.
await self.channel_layer.group_send(
group='drivers', message={
'type': 'echo.message',
'data': trip_data
}
)
# Add trip to set.
if trip_id not in self.trips:
self.trips.add(trip_id)
# Add this channel to the new trip's group.
await self.channel_layer.group_add(
group=trip_id, channel=self.channel_name
)
await self.send_json({
'type': 'create.trip',
'data': trip_data
})
# new
@database_sync_to_async
def _create_trip(self, content):
serializer = TripSerializer(data=content)
serializer.is_valid(raise_exception=True)
trip = serializer.create(serializer.validated_data)
return trip
前端 - 創(chuàng)建訂單
點(diǎn)擊導(dǎo)航條上的叫車按鈕撕予,顯示對話框:
<template>
# /src/App.vue
<v-dialog v-model="dialog">
<v-card>
<v-card-title class="headline">你想去哪里?</v-card-title>
<v-card-text>
<v-layout row>
<v-flex xs12 sm8>
<v-text-field
name="from"
label="出發(fā)地點(diǎn)"
v-model="from"
type="text"
required></v-text-field>
</v-flex>
</v-layout>
<v-layout row wrap>
<v-flex xs12 sm8>
<v-text-field
name="dest"
label="目的地"
v-model="dest"
type="text"
required></v-text-field>
</v-flex>
</v-layout>
</v-card-text>
<v-card-actions>
<v-btn
color="red"
flat="flat"
@click="dialog = false"
>
Cancel
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="green"
flat outline
:disabled="!(from && dest)"
@click="dialog = false; callTaxi()"
>
叫車
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
點(diǎn)擊對話框里的“叫車”時蜈首,調(diào)用Vuex的createTrip
action來發(fā)送WebSockets消息:
<script>
data () {
return {
dialog: false,
from: '',
dest: ''
}
},
methods: {
...mapActions(['clearAlert']),
menu_click (title) {
if (title === 'Exit') {
this.$store.dispatch('messages/signUserOut')
} else if (title === 'Call') {
this.dialog = true
}
},
callTaxi () {
let data = { pick_up_address: this.from, drop_off_address: this.dest, rider: this.user.id }
this.$store.dispatch('ws/createTrip', data)
}
}
同axios实抡,所有與后臺交互的WS操作,全部集中到wsService.js
中欢策,方便管理和更新吆寨。
# /src/services/wsService.js
// send Websockets msg to server
export default {
async createTrip (ws, payload) {
let data = JSON.stringify({
type: 'create.trip',
data: payload
})
await ws.send(data)
}
}
然后,Vuex store里踩寇,根據(jù)需求鸟废,添加不同的actions:
# /src/store/modules/ws.js
const actions = {
async createTrip ({ commit }, message) {
await wsService.createTrip(state.websocket.ws, message)
},
async updateTrip ({ commit }, message) {
await wsService.updateTrip(state.websocket.ws, message)
}
}
測試:按F12,瀏覽器Console窗口姑荷,點(diǎn)叫車按鈕,輸入數(shù)據(jù)缩擂,就能看到創(chuàng)建成功的WS消息了:
WS received: {
"type":"create.trip",
"data":{
"id":"69caf2d4-a9cb-4b3e-80d3-2412a2debe99","driver":null,
"rider":{"id":2,"username":"rider1","first_name":"","last_name":""},
"created":"2019-05-19T11:40:41.278098Z",
"updated":"2019-05-19T11:40:41.278126Z",
"pick_up_address":"南京",
"drop_off_address":"大理",
"status":"REQUESTED"}
}
收到后臺WS消息后鼠冕,setAlert消息,并且更新“當(dāng)前訂單”胯盯。這是前端業(yè)務(wù)邏輯懈费,集中放在ws.js
中
# /src/store.modules/ws.js
const actions = {
// handle msg from server
wsOnMessage ({ dispatch, commit }, e) {
const rdata = JSON.parse(e.data)
console.log('WS received: ' + JSON.stringify(rdata))
switch (rdata.type) {
case 'create.trip':
commit('messages/addTrip', rdata.data, { root: true })
break
case 'update.trip':
break
}
},
添加addTrip action,并且我們讓trips按更新時間逆序排序:
# /scr/store/modules/messages.js
const getters = {
trips: state => {
return state.trips.sort((a, b) => new Date(b.updated) - new Date(a.updated))
},
}
const mutations = {
addTrip (state, messages) {
state.trips.splice(0, 0, message)
},
Channels 更新消息的群發(fā)群收
用戶創(chuàng)建訂單后博脑,如果有司機(jī)接單憎乙,則用戶應(yīng)能即時得到通知票罐。
用戶退出時,司機(jī)也能收到通知泞边。
實(shí)現(xiàn):利用Channels group
Consumer
- 每個用戶该押,維護(hù)一個trips列表
- 在新訂單創(chuàng)建后,
channel_layer.group_add
來新建一個group - 群阵谚,在群內(nèi)的所有成員(乘客和司機(jī))蚕礼,會同時收到更新提醒 - 用戶WS連接關(guān)閉(可能是退出程序,也可能是無信號)梢什,則Channels里解散用戶所處的群奠蹬,并把trips列表清空
# backend/api/consumers.py
import asyncio # new
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from api.serializers import ReadOnlyTripSerializer, TripSerializer
class TaxiConsumer(AsyncJsonWebsocketConsumer):
# new
def __init__(self, scope):
super().__init__(scope)
# Keep track of the user's trips.
self.trips = set()
async def connect(self): ...
async def receive_json(self, content, **kwargs): ...
# new
async def echo_message(self, event):
await self.send_json(event)
# changed
async def create_trip(self, event):
trip = await self._create_trip(event.get('data'))
trip_id = f'{trip.id}'
trip_data = ReadOnlyTripSerializer(trip).data
# Add trip to set.
self.trips.add(trip_id)
# Add this channel to the new trip's group.
await self.channel_layer.group_add(
group=trip_id,
channel=self.channel_name
)
await self.send_json({
'type': 'create.trip',
'data': trip_data
})
# new
async def disconnect(self, code):
# Remove this channel from every trip's group.
channel_groups = [
self.channel_layer.group_discard(
group=trip,
channel=self.channel_name
)
for trip in self.trips
]
asyncio.gather(*channel_groups)
# Remove all references to trips.
self.trips.clear()
await super().disconnect(code)
@database_sync_to_async
def _create_trip(self, content): ...
用戶恢復(fù)WS連接時,應(yīng)該能從數(shù)據(jù)庫里嗡午,讀取已有trip囤躁,然后重新添加用戶到群里
Consumer
-
_get_trips
讀取數(shù)據(jù)庫記錄,排除已完成的訂單 -
channel_layer.group_add
添加用戶到所有未完成訂單的群里
# api/consumers.py
import asyncio
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from api.models import Trip # new
from api.serializers import ReadOnlyTripSerializer, TripSerializer
class TaxiConsumer(AsyncJsonWebSocketConsumer):
def __init__(self, scope): ...
# changed
async def connect(self):
user = self.scope['user']
if user.is_anonymous:
await self.close()
else:
# Get trips and add rider to each one's group.
channel_groups = []
self.trips = set([
str(trip_id) for trip_id in await self._get_trips(self.scope['user'])
])
for trip in self.trips:
channel_groups.append(self.channel_layer.group_add(trip, self.channel_name))
asyncio.gather(*channel_groups)
await self.accept()
async def receive_json(self, content, **kwargs): ...
async def echo_message(self, event): ...
async def create_trip(self, event): ...
async def disconnect(self, code): ...
@database_sync_to_async
def _create_trip(self, content): ...
# new
@database_sync_to_async
def _get_trips(self, user):
if not user.is_authenticated:
raise Exception('User is not authenticated.')
user_groups = user.groups.values_list('name', flat=True)
if 'driver' in user_groups:
return user.trips_as_driver.exclude(
status=Trip.COMPLETED
).only('id').values_list('id', flat=True)
else:
return user.trips_as_rider.exclude(
status=Trip.COMPLETED
).only('id').values_list('id', flat=True)
創(chuàng)建訂單時荔睹,檢查是否已存在記錄狸演。如果已存在,則跳過加群的步驟应媚。
# api/consumers.py
async def create_trip(self, event):
trip = await self._create_trip(event.get('data'))
trip_id = f'{trip.id}'
trip_data = ReadOnlyTripSerializer(trip).data
# Handle add only if trip is not being tracked.
if trip_id not in self.trips:
self.trips.add(trip_id)
await self.channel_layer.group_add(
group=trip_id,
channel=self.channel_name
)
await self.send_json({
'type': 'create.trip',
'data': trip_data
})
更新訂單
Consumer
- 如果司機(jī)/乘客更新了訂單严沥,則觸發(fā)
update_trip
動作 - 通過Serializer,更新訂單狀態(tài)
- 如果是司機(jī)接單中姜,則把司機(jī)加入到群里
channel_layer.group_add()
- 通知乘客消玄,已有司機(jī)接單。
(group=trip_id, message={ 'type': 'echo.message', 'data': trip_data })
- 注意
message={'type': 'echo.message'
丢胚,Channels會自動尋找對應(yīng)的方法函數(shù):echo_message(event)
- 注意
# api/consumers.py
import asyncio
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from api.models import Trip
from api.serializers import ReadOnlyTripSerializer, TripSerializer
class TaxiConsumer(AsyncJsonWebsocketConsumer):
def __init__(self, scope): ...
async def connect(self): ...
async def receive_json(self, content, **kwargs):
message_type = content.get('type')
if message_type == 'create.trip':
await self.create_trip(content)
elif message_type == 'update.trip': # new
await self.update_trip(content)
async def echo_message(self, event): ...
async def create_trip(self, event): ...
# new
async def update_trip(self, event):
trip = await self._update_trip(event.get('data'))
trip_id = f'{trip.id}'
trip_data = ReadOnlyTripSerializer(trip).data
# Send updates to riders that subscribe to this trip.
await self.channel_layer.group_send(group=trip_id, message={
'type': 'echo.message',
'data': trip_data
})
if trip_id not in self.trips:
self.trips.add(trip_id)
await self.channel_layer.group_add(
group=trip_id,
channel=self.channel_name
)
await self.send_json({
'type': 'update.trip',
'data': trip_data
})
async def disconnect(self, code): ...
@database_sync_to_async
def _create_trip(self, content): ...
@database_sync_to_async
def _get_trips(self, user): ...
# new
@database_sync_to_async
def _update_trip(self, content):
instance = Trip.objects.get(id=content.get('id'))
# https://www.django-rest-framework.org/api-guide/serializers/#partial-updates
serializer = TripSerializer(data=content, partial=True)
serializer.is_valid(raise_exception=True)
trip = serializer.update(instance, serializer.validated_data)
return trip
引入U(xiǎn)ser group概念
為了區(qū)分用戶是乘客還是司機(jī)翩瓜,需要把用戶分組。
數(shù)據(jù)模型添加group
計(jì)算字段携龟,類似于Vue computed():
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
DRF Serializer在注冊時兔跌,增加group字段的處理:
# /backend/api/serializers.py
class UserSerializer(serializers.ModelSerializer):
password1 = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True)
group = serializers.CharField()
# photo = MediaImageField(allow_empty_file=True)
def validate(self, data):
if data['password1'] != data['password2']:
raise serializers.ValidationError('兩次密碼不一致')
return data
def create(self, validated_data):
group_data = validated_data.pop('group')
group, _ = Group.objects.get_or_create(name=group_data)
data = {
key: value for key, value in validated_data.items()
if key not in ('password1', 'password2')
}
data['password'] = validated_data['password1']
user = self.Meta.model.objects.create_user(**data)
user.groups.add(group)
user.save()
return user
class Meta:
model = get_user_model()
fields = (
'id', 'username', 'password1', 'password2', 'first_name', 'last_name', 'group', #'photo',
)
read_only_fields = ('id',)
admin后臺管理頁面:
# /backend/api/admin.py
@admin.register(User)
class UserAdmin(DefaultUserAdmin):
list_display = (
'username', 'id', 'group', 'first_name', 'last_name', 'email', 'is_staff',
)
readonly_fields = (
'id',
)
注意:數(shù)據(jù)庫不需要重新migrate,應(yīng)該不是新字段峡蟋,而且計(jì)算字段坟桅。
注意:已有用戶,需要在admin里添加“group”字段蕊蝗〗雠遥或者刪除重新注冊。
前端Sign-Up頁面
我們在注冊用戶時蓬戚,讓用戶選擇不同角色:
更新一下Vue view:
<template>
# /src/views/Signup.vue
<v-radio-group v-model="group" row>
<v-radio label="乘客" value="rider"></v-radio>
<v-radio label="司機(jī)" value="driver"></v-radio>
</v-radio-group>
<v-layout>
<v-flex xs12>
<v-card-actions>
<v-spacer />
<v-btn round type="submit" :loading="loading" class="orange">Register</v-btn>
</v-card-actions>
</v-flex>
</v-layout>
<script>
data () {
return {
username: '',
password: '',
confirmPassword: '',
group: 'rider'
}
},
methods: {
onSignup () {
this.$store.dispatch('messages/signUserUp', { username: this.username, password2: this.confirmPassword, password1: this.password, group: this.group })
},
總結(jié)
后臺對訂單的更新夸楣、群發(fā)群收,已經(jīng)全部ready了。
下一篇豫喧,會介紹前端如何處理訂單更新
帶你進(jìn)入異步Django+Vue的世界 - Didi打車實(shí)戰(zhàn)(6)