forked from p4b8lshcr/ChefTronic
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
12 KiB
470 lines
12 KiB
/**
|
|
* 预订控制器
|
|
*
|
|
* 功能:处理预订管理的业务逻辑
|
|
*/
|
|
|
|
const Reservation = require('../models/Reservation');
|
|
const { validateRequired, validatePhone, validateBatch } = require('../utils/validator');
|
|
const { success } = require('../utils/response');
|
|
const { BusinessError, ValidationError, AuthorizationError } = require('../middleware/errorHandler');
|
|
|
|
/**
|
|
* 获取预订列表(分页、筛选)
|
|
* GET /api/reservations
|
|
*/
|
|
const getReservations = async (req, res, next) => {
|
|
try {
|
|
const {
|
|
page = 1,
|
|
pageSize = 10,
|
|
type,
|
|
status,
|
|
date,
|
|
table_number
|
|
} = req.query;
|
|
|
|
// 权限控制:顾客只能查看自己的预订
|
|
const customer_id = req.user.role === 'customer' ? req.user.id : undefined;
|
|
|
|
const result = await Reservation.findAll({
|
|
page: parseInt(page),
|
|
pageSize: parseInt(pageSize),
|
|
customer_id,
|
|
type,
|
|
status,
|
|
date,
|
|
table_number
|
|
});
|
|
|
|
res.json(success({
|
|
list: result.list,
|
|
pagination: {
|
|
total: result.total,
|
|
page: result.page,
|
|
pageSize: result.pageSize,
|
|
totalPages: Math.ceil(result.total / result.pageSize)
|
|
}
|
|
}, '获取预订列表成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取单个预订详情
|
|
* GET /api/reservations/:id
|
|
*/
|
|
const getReservationById = async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const reservation = await Reservation.findById(id);
|
|
if (!reservation) {
|
|
throw new BusinessError('预订不存在', 404);
|
|
}
|
|
|
|
// 权限控制:顾客只能查看自己的预订
|
|
if (req.user.role === 'customer' && reservation.customer_id !== req.user.id) {
|
|
throw new AuthorizationError('您没有权限查看此预订');
|
|
}
|
|
|
|
res.json(success(reservation, '获取预订详情成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 创建预订
|
|
* POST /api/reservations
|
|
*/
|
|
const createReservation = async (req, res, next) => {
|
|
try {
|
|
const {
|
|
type,
|
|
reservation_time,
|
|
guest_count,
|
|
table_number,
|
|
contact_name,
|
|
contact_phone,
|
|
special_request
|
|
} = req.body;
|
|
|
|
// 数据验证
|
|
const validation = validateBatch([
|
|
validateRequired(type, '预订类型'),
|
|
validateRequired(reservation_time, '预订时间'),
|
|
validateRequired(guest_count, '客人数量'),
|
|
validateRequired(contact_name, '联系人姓名'),
|
|
validateRequired(contact_phone, '联系电话'),
|
|
validatePhone(contact_phone, '联系电话')
|
|
]);
|
|
|
|
if (!validation.valid) {
|
|
throw new ValidationError('数据验证失败', validation.errors);
|
|
}
|
|
|
|
// 验证预订类型
|
|
const validTypes = ['dine_in', 'takeaway'];
|
|
if (!validTypes.includes(type)) {
|
|
throw new ValidationError('预订类型必须是 dine_in 或 takeaway');
|
|
}
|
|
|
|
// 验证客人数量
|
|
if (parseInt(guest_count) <= 0) {
|
|
throw new ValidationError('客人数量必须大于0');
|
|
}
|
|
|
|
// 验证预订时间不能是过去时间
|
|
const reservationDate = new Date(reservation_time);
|
|
if (reservationDate < new Date()) {
|
|
throw new ValidationError('预订时间不能早于当前时间');
|
|
}
|
|
|
|
// 堂食预订必须有桌号
|
|
if (type === 'dine_in') {
|
|
if (!table_number) {
|
|
throw new ValidationError('堂食预订必须指定桌号');
|
|
}
|
|
|
|
// 检查桌位是否可用
|
|
const isAvailable = await Reservation.isTableAvailable(table_number, reservation_time);
|
|
if (!isAvailable) {
|
|
throw new BusinessError('该桌位在所选时间段内已被预订,请选择其他桌位或时间');
|
|
}
|
|
}
|
|
|
|
const reservation = await Reservation.create({
|
|
customer_id: req.user.id,
|
|
type,
|
|
reservation_time,
|
|
guest_count: parseInt(guest_count),
|
|
table_number: type === 'dine_in' ? parseInt(table_number) : null,
|
|
contact_name,
|
|
contact_phone,
|
|
special_request: special_request || null
|
|
});
|
|
|
|
res.status(201).json(success(reservation, '创建预订成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 更新预订
|
|
* PUT /api/reservations/:id
|
|
*/
|
|
const updateReservation = async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const {
|
|
reservation_time,
|
|
guest_count,
|
|
table_number,
|
|
contact_name,
|
|
contact_phone,
|
|
special_request
|
|
} = req.body;
|
|
|
|
// 检查预订是否存在
|
|
const reservation = await Reservation.findById(id);
|
|
if (!reservation) {
|
|
throw new BusinessError('预订不存在', 404);
|
|
}
|
|
|
|
// 权限控制:顾客只能修改自己的预订
|
|
if (req.user.role === 'customer' && reservation.customer_id !== req.user.id) {
|
|
throw new AuthorizationError('您没有权限修改此预订');
|
|
}
|
|
|
|
// 已完成或已取消的预订不能修改
|
|
if (['completed', 'cancelled', 'no_show'].includes(reservation.status)) {
|
|
throw new BusinessError(`${reservation.status === 'completed' ? '已完成' : reservation.status === 'cancelled' ? '已取消' : '未到场'}的预订不能修改`);
|
|
}
|
|
|
|
// 验证数据
|
|
if (guest_count !== undefined && parseInt(guest_count) <= 0) {
|
|
throw new ValidationError('客人数量必须大于0');
|
|
}
|
|
|
|
if (contact_phone && !validatePhone(contact_phone).valid) {
|
|
throw new ValidationError('联系电话格式不正确');
|
|
}
|
|
|
|
// 如果修改了预订时间,检查是否为过去时间
|
|
const newReservationTime = reservation_time || reservation.reservation_time;
|
|
if (new Date(newReservationTime) < new Date()) {
|
|
throw new ValidationError('预订时间不能早于当前时间');
|
|
}
|
|
|
|
// 如果是堂食且修改了桌号或时间,检查桌位是否可用
|
|
if (reservation.type === 'dine_in' && (table_number || reservation_time)) {
|
|
const newTableNumber = table_number || reservation.table_number;
|
|
const isAvailable = await Reservation.isTableAvailable(
|
|
newTableNumber,
|
|
newReservationTime,
|
|
id
|
|
);
|
|
|
|
if (!isAvailable) {
|
|
throw new BusinessError('该桌位在所选时间段内已被预订,请选择其他桌位或时间');
|
|
}
|
|
}
|
|
|
|
const updatedReservation = await Reservation.update(id, {
|
|
reservation_time,
|
|
guest_count: guest_count !== undefined ? parseInt(guest_count) : undefined,
|
|
table_number: table_number !== undefined ? (table_number ? parseInt(table_number) : null) : undefined,
|
|
contact_name,
|
|
contact_phone,
|
|
special_request: special_request !== undefined ? special_request : undefined
|
|
});
|
|
|
|
res.json(success(updatedReservation, '更新预订成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 更新预订状态
|
|
* PUT /api/reservations/:id/status
|
|
* 需要管理员或服务员权限
|
|
*/
|
|
const updateReservationStatus = async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { status } = req.body;
|
|
|
|
// 数据验证
|
|
const validation = validateRequired(status, '状态');
|
|
if (!validation.valid) {
|
|
throw new ValidationError('数据验证失败', validation.errors);
|
|
}
|
|
|
|
// 验证状态值
|
|
const validStatuses = ['pending', 'confirmed', 'completed', 'cancelled', 'no_show'];
|
|
if (!validStatuses.includes(status)) {
|
|
throw new ValidationError(`状态必须是以下之一: ${validStatuses.join(', ')}`);
|
|
}
|
|
|
|
// 检查预订是否存在
|
|
const reservation = await Reservation.findById(id);
|
|
if (!reservation) {
|
|
throw new BusinessError('预订不存在', 404);
|
|
}
|
|
|
|
// 已完成或未到场的预订不能修改状态
|
|
if (['completed', 'no_show'].includes(reservation.status)) {
|
|
throw new BusinessError(`${reservation.status === 'completed' ? '已完成' : '未到场'}的预订不能修改状态`);
|
|
}
|
|
|
|
const updatedReservation = await Reservation.updateStatus(id, status);
|
|
|
|
const messages = {
|
|
pending: '预订已标记为待确认',
|
|
confirmed: '预订已确认',
|
|
completed: '预订已完成',
|
|
cancelled: '预订已取消',
|
|
no_show: '预订已标记为未到场'
|
|
};
|
|
|
|
res.json(success(updatedReservation, messages[status]));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 取消预订
|
|
* PUT /api/reservations/:id/cancel
|
|
*/
|
|
const cancelReservation = async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { cancel_reason } = req.body;
|
|
|
|
// 检查预订是否存在
|
|
const reservation = await Reservation.findById(id);
|
|
if (!reservation) {
|
|
throw new BusinessError('预订不存在', 404);
|
|
}
|
|
|
|
// 权限控制:顾客只能取消自己的预订,管理员和服务员可以取消任何预订
|
|
if (req.user.role === 'customer' && reservation.customer_id !== req.user.id) {
|
|
throw new AuthorizationError('您没有权限取消此预订');
|
|
}
|
|
|
|
// 已完成、已取消或未到场的预订不能再次取消
|
|
if (['completed', 'cancelled', 'no_show'].includes(reservation.status)) {
|
|
throw new BusinessError('该预订已完成或已取消');
|
|
}
|
|
|
|
const updatedReservation = await Reservation.cancel(id, cancel_reason || null);
|
|
|
|
res.json(success(updatedReservation, '取消预订成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 删除预订
|
|
* DELETE /api/reservations/:id
|
|
* 需要管理员权限
|
|
*/
|
|
const deleteReservation = async (req, res, next) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// 检查预订是否存在
|
|
const reservation = await Reservation.findById(id);
|
|
if (!reservation) {
|
|
throw new BusinessError('预订不存在', 404);
|
|
}
|
|
|
|
await Reservation.delete(id);
|
|
|
|
res.json(success(null, '删除预订成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取今日预订列表
|
|
* GET /api/reservations/today
|
|
*/
|
|
const getTodayReservations = async (req, res, next) => {
|
|
try {
|
|
const { type } = req.query;
|
|
|
|
const reservations = await Reservation.getTodayReservations(type || null);
|
|
|
|
res.json(success({
|
|
count: reservations.length,
|
|
reservations
|
|
}, '获取今日预订成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取即将到来的预订
|
|
* GET /api/reservations/upcoming
|
|
*/
|
|
const getUpcomingReservations = async (req, res, next) => {
|
|
try {
|
|
// 权限控制:顾客只能查看自己的预订
|
|
const customer_id = req.user.role === 'customer' ? req.user.id : null;
|
|
|
|
const reservations = await Reservation.getUpcomingReservations(customer_id);
|
|
|
|
res.json(success({
|
|
count: reservations.length,
|
|
reservations
|
|
}, '获取即将到来的预订成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取我的预订列表
|
|
* GET /api/reservations/my
|
|
*/
|
|
const getMyReservations = async (req, res, next) => {
|
|
try {
|
|
const {
|
|
page = 1,
|
|
pageSize = 10,
|
|
status
|
|
} = req.query;
|
|
|
|
const result = await Reservation.findAll({
|
|
page: parseInt(page),
|
|
pageSize: parseInt(pageSize),
|
|
customer_id: req.user.id,
|
|
status
|
|
});
|
|
|
|
res.json(success({
|
|
list: result.list,
|
|
pagination: {
|
|
total: result.total,
|
|
page: result.page,
|
|
pageSize: result.pageSize,
|
|
totalPages: Math.ceil(result.total / result.pageSize)
|
|
}
|
|
}, '获取我的预订成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取可用桌位
|
|
* GET /api/reservations/available-tables
|
|
*/
|
|
const getAvailableTables = async (req, res, next) => {
|
|
try {
|
|
const { reservation_time, guest_count } = req.query;
|
|
|
|
// 数据验证
|
|
const validation = validateRequired(reservation_time, '预订时间');
|
|
if (!validation.valid) {
|
|
throw new ValidationError('数据验证失败', validation.errors);
|
|
}
|
|
|
|
const tables = await Reservation.getAvailableTables(
|
|
reservation_time,
|
|
guest_count ? parseInt(guest_count) : null
|
|
);
|
|
|
|
res.json(success({
|
|
count: tables.length,
|
|
tables
|
|
}, '获取可用桌位成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 获取预订统计
|
|
* GET /api/reservations/statistics
|
|
*/
|
|
const getStatistics = async (req, res, next) => {
|
|
try {
|
|
const { start_date, end_date } = req.query;
|
|
|
|
const stats = await Reservation.getStatistics(
|
|
start_date || null,
|
|
end_date || null
|
|
);
|
|
|
|
res.json(success(stats, '获取预订统计成功'));
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
};
|
|
|
|
// 导出所有控制器函数
|
|
module.exports = {
|
|
getReservations,
|
|
getReservationById,
|
|
createReservation,
|
|
updateReservation,
|
|
updateReservationStatus,
|
|
cancelReservation,
|
|
deleteReservation,
|
|
getTodayReservations,
|
|
getUpcomingReservations,
|
|
getMyReservations,
|
|
getAvailableTables,
|
|
getStatistics
|
|
};
|