From cabed63d2a331079b5bb686932b1bc7a3f827230 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 7 May 2019 15:56:43 +0800 Subject: [PATCH] finish the booking flow with single thread queue --- BookingService/__init__.py | 7 + BookingService/celery.py | 19 ++ BookingService/settings.py | 9 + Pipfile | 3 + booking/serializers.py | 7 + booking/tasks.py | 45 ++++ booking/urls.py | 3 +- booking/views.py | 37 ++- frontend/src/api/booking.js | 6 + frontend/src/views/account/Account.vue | 2 +- frontend/src/views/booking/Booking.vue | 2 +- frontend/src/views/booking/BookingDetail.vue | 230 +++++++++++++++--- .../src/views/dashboard/DashboardDetail.vue | 35 +-- .../dashboard/components/DatePickerBar.vue | 22 +- user/views.py | 1 + 15 files changed, 373 insertions(+), 55 deletions(-) create mode 100644 BookingService/celery.py create mode 100644 booking/tasks.py diff --git a/BookingService/__init__.py b/BookingService/__init__.py index e69de29..070e835 100644 --- a/BookingService/__init__.py +++ b/BookingService/__init__.py @@ -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',) diff --git a/BookingService/celery.py b/BookingService/celery.py new file mode 100644 index 0000000..761bc93 --- /dev/null +++ b/BookingService/celery.py @@ -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() diff --git a/BookingService/settings.py b/BookingService/settings.py index 362a81a..c5e6b08 100644 --- a/BookingService/settings.py +++ b/BookingService/settings.py @@ -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' diff --git a/Pipfile b/Pipfile index 8652cd2..7f28618 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,9 @@ verify_ssl = true django = "*" djangorestframework = "*" django-filter = "*" +flower = "*" +celery = "*" +eventlet = "*" [requires] python_version = "3.7" diff --git a/booking/serializers.py b/booking/serializers.py index 2575822..e7e9f0b 100644 --- a/booking/serializers.py +++ b/booking/serializers.py @@ -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'] diff --git a/booking/tasks.py b/booking/tasks.py new file mode 100644 index 0000000..e9bc076 --- /dev/null +++ b/booking/tasks.py @@ -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() diff --git a/booking/urls.py b/booking/urls.py index cec559d..b1ae44a 100644 --- a/booking/urls.py +++ b/booking/urls.py @@ -6,7 +6,8 @@ app_name = 'booking' urlpatterns = [ path('', views.Booking.as_view()), - path('/', views.BookingDetail.as_view()), + path('/', views.BookingDetail.as_view()), + path('/cancel/', views.BookingCancel.as_view()), path('statusList/', views.BookingStatusList.as_view()), path('setting//', views.SettingDetail.as_view()), path('room/', views.Room.as_view()), diff --git a/booking/views.py b/booking/views.py index a7cb07c..15ba06e 100644 --- a/booking/views.py +++ b/booking/views.py @@ -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): """ 获得房间预约信息 diff --git a/frontend/src/api/booking.js b/frontend/src/api/booking.js index 49b6ade..6c34ded 100644 --- a/frontend/src/api/booking.js +++ b/frontend/src/api/booking.js @@ -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') } diff --git a/frontend/src/views/account/Account.vue b/frontend/src/views/account/Account.vue index b4a10ac..ddba175 100644 --- a/frontend/src/views/account/Account.vue +++ b/frontend/src/views/account/Account.vue @@ -39,7 +39,7 @@ diff --git a/frontend/src/views/booking/Booking.vue b/frontend/src/views/booking/Booking.vue index 41dc457..7bfb7b7 100644 --- a/frontend/src/views/booking/Booking.vue +++ b/frontend/src/views/booking/Booking.vue @@ -94,7 +94,7 @@ diff --git a/frontend/src/views/booking/BookingDetail.vue b/frontend/src/views/booking/BookingDetail.vue index 46fcd7d..c2434a0 100644 --- a/frontend/src/views/booking/BookingDetail.vue +++ b/frontend/src/views/booking/BookingDetail.vue @@ -5,25 +5,53 @@ :xs="{span: 24}" :sm="{span: 20}" :xl="{span: 18}"> + 预约信息 - {{newBooking.date}} + {{bookingEdit.id}} - {{newBooking.room.name}} + + {{isEdit ? bookingEdit.room.name : bookingCreate.room.name}} + -
座位: {{item.seat.name}}
-
开始时间: {{item.start_time}}
-
结束时间: {{item.end_time}}
+ {{isEdit ? bookingEdit.date : bookingCreate.date}}
+ + {{isEdit ? bookingEdit.start_time : bookingCreate.start_time}} + + + {{isEdit ? bookingEdit.end_time : bookingCreate.end_time}} + +
+ +
{{item.name}}
+
+
+
+ +
{{item.name}}
+
+
@@ -34,14 +62,98 @@ :filterOption="false" :notFoundContent="null" @search="handleSearch" - v-model="form.user"> + v-if="!isEdit" + v-model="createBookingForm.user"> {{item.username}} {{item.email}} + + {{bookingEdit.user.username}} {{bookingEdit.user.email}} + + + + {{bookingEdit.created_datetime}} + + + {{bookingEdit.arrive_time || '未签到'}} + + + {{bookingEdit.leave_time || '未签离'}} + + + {{statusName}} + + + + 提交申请 + + + 取消 + + +
+ 取消预约 + + + {{bookingEdit.cancel_by ? bookingEdit.cancel_by.username : ''}} + + + {{bookingEdit.cancel_datetime}} + + + + - - 提交 + + + 取消预约 + + + 取消预约 + + + 返回 @@ -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 } } } diff --git a/frontend/src/views/dashboard/DashboardDetail.vue b/frontend/src/views/dashboard/DashboardDetail.vue index b8b106a..63b7877 100644 --- a/frontend/src/views/dashboard/DashboardDetail.vue +++ b/frontend/src/views/dashboard/DashboardDetail.vue @@ -5,7 +5,7 @@
- +
新建预约 @@ -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; } diff --git a/frontend/src/views/dashboard/components/DatePickerBar.vue b/frontend/src/views/dashboard/components/DatePickerBar.vue index 4ef55dd..69c3ea0 100644 --- a/frontend/src/views/dashboard/components/DatePickerBar.vue +++ b/frontend/src/views/dashboard/components/DatePickerBar.vue @@ -13,16 +13,23 @@ format="YYYY 年 MM 月 DD 日" @change="handleDateChange" @openChange="handleDatePickerClose" + :disabledDate="disabledDate" :allowClear="false" :value="date"> - 后一天 + + 后一天 diff --git a/user/views.py b/user/views.py index f822045..fa09fcc 100644 --- a/user/views.py +++ b/user/views.py @@ -29,6 +29,7 @@ class User(generics.ListCreateAPIView): 'role', ] search_fields = [ + 'id', 'username', 'email', ]