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.

487 lines
13 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_type,
reservation_time,
reservation_date,
guest_count,
party_size,
table_number,
contact_name,
customer_name,
contact_phone,
phone,
special_request,
notes
} = req.body;
// Map frontend fields to backend fields
const reservationType = reservation_type || type;
const peopleCount = party_size || guest_count;
const contactPhone = phone || contact_phone;
const specialRequest = notes || special_request;
// Handle date and time - keep them separate for the database
const resDate = reservation_date;
const resTime = req.body.reservation_time;
// 数据验证
const validationFields = [
validateRequired(reservationType, '预订类型'),
validateRequired(resDate, '预订日期'),
validateRequired(resTime, '预订时间'),
validateRequired(contactPhone, '联系电话'),
validatePhone(contactPhone, '联系电话')
];
// Only require people_count for table reservations
if (reservationType === 'table' && peopleCount) {
validationFields.push(validateRequired(peopleCount, '客人数量'));
}
const validation = validateBatch(validationFields);
if (!validation.valid) {
throw new ValidationError('数据验证失败', validation.errors);
}
// Validate reservation type matches database enum
const validTypes = ['table', 'takeaway'];
if (!validTypes.includes(reservationType)) {
throw new ValidationError('预订类型必须是 table 或 takeaway');
}
// 验证客人数量(仅当提供时)
if (peopleCount && parseInt(peopleCount) <= 0) {
throw new ValidationError('客人数量必须大于0');
}
// 验证预订时间不能是过去时间
const reservationDateTime = new Date(`${resDate} ${resTime}`);
if (reservationDateTime < new Date()) {
throw new ValidationError('预订时间不能早于当前时间');
}
// 如果是堂食预订且提供了桌号,检查桌位是否可用
if (reservationType === 'table' && table_number) {
const isAvailable = await Reservation.isTableAvailable(table_number, `${resDate} ${resTime}`);
if (!isAvailable) {
throw new BusinessError('该桌位在所选时间段内已被预订,请选择其他桌位或时间');
}
}
const reservation = await Reservation.create({
customer_id: req.user.id,
type: reservationType,
reservation_date: resDate,
reservation_time: resTime,
people_count: peopleCount ? parseInt(peopleCount) : null,
table_number: table_number || null,
contact_phone: contactPhone,
special_request: specialRequest || 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
};