finish the booking flow with single thread queue

This commit is contained in:
David 2019-05-07 15:56:43 +08:00
parent c25e540668
commit cabed63d2a
15 changed files with 373 additions and 55 deletions

View File

@ -0,0 +1,7 @@
from __future__ import absolute_import, unicode_literals
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

19
BookingService/celery.py Normal file
View File

@ -0,0 +1,19 @@
from __future__ import absolute_import, unicode_literals
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'BookingService.settings')
app = Celery('BookingService')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

View File

@ -146,6 +146,15 @@ SILENCED_SYSTEM_CHECKS = [
'rest_framework.W001'
]
# COOKIE
SESSION_COOKIE_HTTPONLY = False
# SLASH
APPEND_SLASH = True
# CELERY
CELERY_BROKER_URL = 'amqp://guest:guest@davidz.cn'
CELERY_TIMEZONE = 'Asia/Shanghai'

View File

@ -9,6 +9,9 @@ verify_ssl = true
django = "*"
djangorestframework = "*"
django-filter = "*"
flower = "*"
celery = "*"
eventlet = "*"
[requires]
python_version = "3.7"

View File

@ -43,6 +43,7 @@ class BookingSerializer(serializers.ModelSerializer):
user = UserSerializer()
room = RoomSerializer()
seats = SeatSerializer(many=True)
cancel_by = UserSerializer()
class Meta:
model = models.Booking
@ -53,3 +54,9 @@ class BookingCreateSerializer(serializers.ModelSerializer):
class Meta:
model = models.Booking
fields = '__all__'
class BookingCancelSerializer(serializers.ModelSerializer):
class Meta:
model = models.Booking
fields = ['cancel_reason']

45
booking/tasks.py Normal file
View File

@ -0,0 +1,45 @@
import datetime
from BookingService.celery import app
from . import models
@app.task
def createBooking(booking):
b = models.Booking.objects.get(id=booking['id'])
date = datetime.datetime.strptime(booking['date'], '%Y-%m-%d').date()
pre_booking_interval_day = models.Setting.objects.get(id=1).pre_booking_interval_day
today = datetime.datetime.now()
max_date = today.date() + datetime.timedelta(days=pre_booking_interval_day)
if date < today.date():
b.status = 'FAILED'
b.cancel_reason = f'预约日期超过限制,最早可预约至{date.strftime("%Y-%m-%d")}'
b.cancel_datetime = today.strftime('%Y-%m-%d %H:%M:%S')
b.save()
return
if date > max_date:
b.status = 'FAILED'
b.cancel_reason = f'预约日期超过限制,最晚可预约至{max_date.strftime("%Y-%m-%d")}'
b.cancel_datetime = today.strftime('%Y-%m-%d %H:%M:%S')
b.save()
return
isDuplicate = False
start_time = datetime.datetime.strptime(booking['start_time'], '%H:%M:%S').time()
end_time = datetime.datetime.strptime(booking['end_time'], '%H:%M:%S').time()
for seat in booking['seats']:
booking_list = models.Booking.objects.filter(room=booking['room'],
date=booking['date'],
status='SUCCESS',
seats__id=seat)
for item in booking_list:
if ((start_time >= item.start_time and start_time <= item.end_time)
or (end_time >= item.start_time and end_time <= item.end_time)
or (start_time <= item.start_time and end_time >= item.end_time)):
isDuplicate = True
if isDuplicate:
b.status = 'FAILED'
b.cancel_reason = '预约冲突。'
b.cancel_datetime = today.strftime('%Y-%m-%d %H:%M:%S')
else:
b.status = 'SUCCESS'
b.save()

View File

@ -6,7 +6,8 @@ app_name = 'booking'
urlpatterns = [
path('', views.Booking.as_view()),
path('<uuid:pk>/', views.BookingDetail.as_view()),
path('<int:pk>/', views.BookingDetail.as_view()),
path('<int:pk>/cancel/', views.BookingCancel.as_view()),
path('statusList/', views.BookingStatusList.as_view()),
path('setting/<int:pk>/', views.SettingDetail.as_view()),
path('room/', views.Room.as_view()),

View File

@ -10,7 +10,7 @@ from rest_framework.filters import SearchFilter, OrderingFilter
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from . import models, serializers, filters
from . import models, serializers, filters, tasks
class SettingDetail(generics.RetrieveUpdateAPIView):
@ -81,6 +81,7 @@ class Booking(generics.ListCreateAPIView):
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
filterset_class = filters.BookingFilter
search_fields = [
'id',
'user__username',
'room__name',
'date'
@ -93,8 +94,16 @@ class Booking(generics.ListCreateAPIView):
]
def perform_create(self, serializer):
print(serializer.data)
return Response('')
data = serializer.validated_data
start_time = data['start_time']
end_time = data['end_time']
setting = models.Setting.objects.get(id=1)
if start_time < setting.start_time or start_time > setting.end_time:
raise ValidationError(f'开始时间无效,应该在[{setting.start_time} - {setting.end_time}]之间。')
if end_time < setting.start_time or end_time > setting.end_time:
raise ValidationError(f'结束时间无效,应该在[{setting.start_time} - {setting.end_time}]之间。')
serializer.save()
tasks.createBooking.delay(serializer.data)
def get_serializer_class(self):
if self.request.stream and self.request.stream.method == 'POST':
@ -131,6 +140,28 @@ class BookingStatusList(generics.GenericAPIView):
return Response(status_list)
class BookingCancel(generics.GenericAPIView):
"""
取消预约
"""
serializer_class = serializers.BookingCancelSerializer
permission_classes = (permissions.IsAdminUser,)
def post(self, request, pk):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
booking = self.get_queryset()
booking.status = 'CANCELED'
booking.cancel_by = request.user
booking.cancel_datetime = datetime.datetime.now()
booking.cancel_reason = serializer.validated_data['cancel_reason']
booking.save()
return Response(serializer.validated_data)
def get_queryset(self):
return models.Booking.objects.get(id=self.kwargs['pk'])
class RoomBookingStatus(generics.GenericAPIView):
"""
获得房间预约信息

View File

@ -7,6 +7,12 @@ export default {
createBooking (booking) {
return fetchAPI('booking/', 'post', booking)
},
getBooking (id) {
return fetchAPI(`booking/${id}/`, 'get')
},
cancelBooking (id, data) {
return fetchAPI(`booking/${id}/cancel/`, 'post', data)
},
getStatusList () {
return fetchAPI('booking/statusList/', 'get')
}

View File

@ -39,7 +39,7 @@
<a-input
v-model="params.search"
style="width: 100%;"
placeholder="用户名,邮箱"
placeholder="ID用户名,邮箱"
@keypress.enter="getData">
</a-input>
</a-col>

View File

@ -94,7 +94,7 @@
<a-input
v-model="params.search"
style="width: 100%;"
placeholder="用户,房间,日期"
placeholder="ID用户,房间,日期"
@keypress.enter="getData">
</a-input>
</a-col>

View File

@ -5,25 +5,53 @@
:xs="{span: 24}"
:sm="{span: 20}"
:xl="{span: 18}">
<a-divider v-if="isEdit">预约信息</a-divider>
<a-form>
<a-form-item
label="日期"
label="ID"
v-if="isEdit"
v-bind="layout">
{{newBooking.date}}
{{bookingEdit.id}}
</a-form-item>
<a-form-item
label="房间"
v-bind="layout">
{{newBooking.room.name}}
<router-link
:to="{ name: 'roomEdit', params: { id: bookingEdit.room.id} }">
{{isEdit ? bookingEdit.room.name : bookingCreate.room.name}}
</router-link>
</a-form-item>
<a-form-item
v-for="(item, index) in newBooking.booking_list" :key="index"
:label="'预约' + (index + 1).toString()"
label="日期"
v-bind="layout">
<div>座位: {{item.seat.name}}</div>
<div>开始时间: {{item.start_time}}</div>
<div>结束时间: {{item.end_time}}</div>
{{isEdit ? bookingEdit.date : bookingCreate.date}}
</a-form-item>
<a-form-item
label="开始时间"
v-bind="layout">
{{isEdit ? bookingEdit.start_time : bookingCreate.start_time}}
</a-form-item>
<a-form-item
label="结束时间"
v-bind="layout">
{{isEdit ? bookingEdit.end_time : bookingCreate.end_time}}
</a-form-item>
<div v-if="bookingEdit">
<a-form-item
v-for="(item, index) in bookingEdit.seats" :key="index"
:label="'座位' + (index + 1).toString()"
v-bind="layout">
<div>{{item.name}}</div>
</a-form-item>
</div>
<div v-if="bookingCreate">
<a-form-item
v-for="(item, index) in bookingCreate.seats" :key="index"
:label="'座位' + (index + 1).toString()"
v-bind="layout">
<div>{{item.name}}</div>
</a-form-item>
</div>
<a-form-item
label="用户"
v-bind="layout">
@ -34,14 +62,98 @@
:filterOption="false"
:notFoundContent="null"
@search="handleSearch"
v-model="form.user">
v-if="!isEdit"
v-model="createBookingForm.user">
<a-select-option v-for="item in userList" :value="item.id">{{item.username}} {{item.email}}
</a-select-option>
</a-select>
<router-link
:to="{ name: 'accountEdit', params: { id: bookingEdit.user.id} }"
v-else>
{{bookingEdit.user.username}} {{bookingEdit.user.email}}
</router-link>
</a-form-item>
<a-form-item
label="申请时间"
v-if="isEdit"
v-bind="layout">
{{bookingEdit.created_datetime}}
</a-form-item>
<a-form-item
label="到达时间"
v-if="isEdit"
v-bind="layout">
{{bookingEdit.arrive_time || '未签到'}}
</a-form-item>
<a-form-item
label="离开时间"
v-if="isEdit"
v-bind="layout">
{{bookingEdit.leave_time || '未签离'}}
</a-form-item>
<a-form-item
label="状态"
v-if="isEdit"
v-bind="layout">
{{statusName}}
</a-form-item>
<a-form-item v-if="!isEdit">
<a-button type="primary" @click="handleCreateSubmit" style="float: right">
提交申请
</a-button>
<a-button @click="$router.go(-1)" style="float: right; margin-right: 8px">
取消
</a-button>
</a-form-item>
</a-form>
<a-divider v-if="isEdit">取消预约</a-divider>
<a-form
v-if="isEdit"
:form="cancelBookingForm">
<a-form-item
label="取消人"
v-bind="layout">
{{bookingEdit.cancel_by ? bookingEdit.cancel_by.username : ''}}
</a-form-item>
<a-form-item
label="取消时间"
v-bind="layout">
{{bookingEdit.cancel_datetime}}
</a-form-item>
<a-form-item
label="取消原因"
v-bind="layout">
<a-textarea
:rows="4"
v-decorator="[
'cancel_reason',
{rules: [
{max: 512, message: '最多512个字符'},
], validateTrigger: 'blur',
initialValue: bookingEdit.cancel_reason}
]">
</a-textarea>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSubmit" style="float: right">
提交
<a-popconfirm
v-if="bookingEdit.status === 'SUCCESS'"
style="float: right"
title="取消预约不可逆"
@confirm="handleCancelSubmit"
okText="确定"
cancelText="关闭">
<a-button
type="primary">
取消预约
</a-button>
</a-popconfirm>
<a-button
v-else
style="float: right"
:disabled="true">取消预约
</a-button>
<a-button @click="$router.go(-1)" style="float: right; margin-right: 8px">
返回
</a-button>
</a-form-item>
</a-form>
@ -55,13 +167,15 @@
import PageLayout from '../../components/page/PageLayout'
import accountAPI from '../../api/account'
import bookingAPI from '../../api/booking'
export default {
name: 'BookingDetail',
components: {
PageLayout
},
props: {
newBooking: Object
id: Number,
bookingCreate: Object
},
data () {
return {
@ -70,38 +184,82 @@
'wrapper-col': { span: 18 }
},
userList: [],
form: {
user: this.$store.getters.userid,
room: this.newBooking.room.id,
seats: [
this.newBooking.booking_list[0].seat.id
],
start_time: this.newBooking.booking_list[0].start_time,
end_time: this.newBooking.booking_list[0].end_time,
date: this.newBooking.date
bookingEdit: {
room: {
id: '0',
name: ''
},
user: {
id: 0,
name: '',
email: ''
}
},
createBookingForm: {
user: this.$store.getters.userid,
},
cancelBookingForm: this.$form.createForm(this),
params: {
search: ''
}
},
statusList: []
}
},
beforeRouteEnter (to, from, next) {
if (from.path !== '/') {
next()
} else {
next({ name: 'dashboard' })
}
},
mounted () {
if (!this.isEdit) {
this.getData()
}
this.getData()
},
methods: {
getData () {
accountAPI.getUserList(this.params)
if (this.isEdit) {
bookingAPI.getBooking(this.id)
.then(data => {
this.bookingEdit = data
})
bookingAPI.getStatusList()
.then(data => {
this.statusList = data
})
} else {
accountAPI.getUserList(this.params)
.then(data => {
this.userList = data.results
})
}
},
handleCreateSubmit () {
let form = {
user: this.createBookingForm.user,
room: this.bookingCreate.room.id,
seats: this.bookingCreate.seats.map((item) => {
return item.id
}),
date: this.bookingCreate.date,
start_time: this.bookingCreate.start_time,
end_time: this.bookingCreate.end_time
}
bookingAPI.createBooking(form)
.then(data => {
this.userList = data.results
this.$notification.success({ message: '成功', description: '提交申请成功', key: 'SUCCESS' })
this.$router.push({ name: 'dashboard' })
})
},
handleSubmit () {
bookingAPI.createBooking(this.form)
.then(data => {
console.log(data)
})
handleCancelSubmit () {
this.cancelBookingForm.validateFields((error, data) => {
if (!error) {
bookingAPI.cancelBooking(this.bookingEdit.id, data)
.then(data => {
this.$notification.success({ message: '成功', description: '取消预约成功', key: 'SUCCESS' })
this.getData()
})
}
})
},
handleSearch (value) {
this.params.search = value
@ -111,6 +269,14 @@
computed: {
isEdit () {
return this.$route.path.split('/').pop() === 'edit'
},
statusName () {
let ret = ''
let result = this.statusList.find(item => item.id === this.bookingEdit.status)
if (result) {
ret = result.name
}
return ret
}
}
}

View File

@ -5,7 +5,7 @@
<div style="text-align: center;">
<a-spin :spinning="status.loading">
<a-row class="booking-status-timetable-wrapper" v-if="room.seat_list">
<table class="booking-status-timetable" border="1">
<table class="booking-status-timetable">
<tr>
<th></th>
<th
@ -52,7 +52,7 @@
<a-button
style="float: right;"
type="primary"
:disabled="firstSelected !== null || Object.keys(selected).length === 0"
:disabled="Object.keys(selected).length === 0"
@click="handleCreate">
新建预约
</a-button>
@ -178,29 +178,30 @@
handleCreate () {
let key_list = Object.keys(this.selected)
let x = key_list[0].split(' ')[0]
let y1 = key_list[0].split(' ')[1]
let y2 = key_list[key_list.length - 1].split(' ')[1]
let booking_list = []
let key_y_list = key_list.map((item) => {
return item.split(' ')[1]
})
let y1 = Math.min.apply(null, key_y_list)
let y2 = Math.max.apply(null, key_y_list)
let seats = []
let seat = this.room.seat_list[x]
let start_time = this.room.seat_list[x].booking_status_list[y1].start_time
let end_time = this.room.seat_list[x].booking_status_list[y2].end_time
booking_list.push({
seat: {
id: seat.id,
name: seat.name
},
start_time,
end_time
seats.push({
id: seat.id,
name: seat.name
})
let newBooking = {
date: this.date,
let bookingCreate = {
date: this.params.date,
room: {
id: this.room.id,
name: this.room.name
},
booking_list
seats,
start_time,
end_time
}
this.$router.push({ name: 'bookingCreate', params: { newBooking } })
this.$router.push({ name: 'bookingCreate', params: { bookingCreate } })
}
}
}
@ -215,7 +216,9 @@
.booking-status-timetable {
th, td {
border: solid 1px rgba(128, 128, 128, 0.3);
padding: 5px 2px;
}

View File

@ -13,16 +13,23 @@
format="YYYY 年 MM 月 DD 日"
@change="handleDateChange"
@openChange="handleDatePickerClose"
:disabledDate="disabledDate"
:allowClear="false"
:value="date">
</a-date-picker>
<a-button class="right" type="primary" @click="handleNext">后一天
<a-button
class="right"
type="primary"
:disabled="date.format('YYYY-MM-DD') === maxDate.format('YYYY-MM-DD')"
@click="handleNext">
后一天
<a-icon type="right"></a-icon>
</a-button>
</a-row>
</template>
<script>
import settingAPI from '../../../api/setting'
import moment from 'moment'
export default {
@ -37,9 +44,14 @@
},
date: this.defaultDate,
dateString: '',
setting: {}
}
},
mounted () {
settingAPI.getSettingDetail()
.then(data => {
this.setting = data
})
this.getDateString()
},
methods: {
@ -64,6 +76,14 @@
this.getDateString()
this.$emit('change', this.date)
},
disabledDate (currentDate) {
return currentDate > this.maxDate
}
},
computed: {
maxDate () {
return new moment().add(this.setting.pre_booking_interval_day, 'days')
}
}
}
</script>

View File

@ -29,6 +29,7 @@ class User(generics.ListCreateAPIView):
'role',
]
search_fields = [
'id',
'username',
'email',
]