rentease-backend-new/controllers/rentalController.js

780 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { Rental, Room, Apartment, Bill, Transaction, Renter, MeterReading } = require('../models');
const { Op } = require('sequelize');
const response = require('../utils/response');
// 格式化时间(考虑时区,转换为北京时间)
const formatDate = (date) => {
if (!date) return null;
// 确保 date 是 Date 对象
const dateObj = date instanceof Date ? date : new Date(date);
if (isNaN(dateObj.getTime())) return null;
// 创建一个新的Date对象加上8小时的时区偏移
const beijingDate = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000);
return beijingDate.toISOString().split('T')[0];
};
// 格式化租房数据
const formatRentalData = (rental) => {
const formattedRental = {
...rental.toJSON(),
startDate: formatDate(rental.startDate),
endDate: formatDate(rental.endDate),
createTime: formatDate(rental.createTime),
updateTime: formatDate(rental.updateTime)
};
return formattedRental;
};
// 检查并更新租房状态
const checkAndUpdateRentalStatus = async () => {
try {
// 获取当前日期
const currentDate = new Date();
// 计算5天后的日期
const fiveDaysLater = new Date();
fiveDaysLater.setDate(currentDate.getDate() + 5);
// 查找所有活跃的租房记录
const rentals = await Rental.findAll({
where: { status: 'active', isDeleted: 0 },
include: [
{
model: Room,
where: { isDeleted: 0 }
}
]
});
// 检查每个租房记录的状态
for (const rental of rentals) {
const endDate = new Date(rental.endDate);
// 检查是否已到期
if (endDate < currentDate) {
// 更新房间租约状态为已到期
const room = await Room.findByPk(rental.roomId);
if (room && room.status === 'rented') {
await room.update({ rentalStatus: 'expired' });
}
} else if (endDate <= fiveDaysLater) {
// 更新房间租约状态为即将到期
const room = await Room.findByPk(rental.roomId);
if (room && room.status === 'rented') {
await room.update({ rentalStatus: 'soon_expire' });
}
} else {
// 更新房间租约状态为正常
const room = await Room.findByPk(rental.roomId);
if (room && room.status === 'rented') {
await room.update({ rentalStatus: 'normal' });
}
}
}
console.log('租房状态检查和更新完成');
} catch (error) {
console.error('检查和更新租房状态时出错:', error);
}
};
// 获取所有租房(支持搜索和分页)
const getAllRentals = async (req, res) => {
try {
// 先检查并更新租房状态
await checkAndUpdateRentalStatus();
const {
apartmentId,
roomId,
renterName,
status,
startDateFrom,
startDateTo,
endDateFrom,
endDateTo,
page = 1,
pageSize = 10
} = req.query;
// 构建查询条件
const where = { isDeleted: 0 };
if (status) {
where.status = status;
}
if (roomId) {
where.roomId = roomId;
}
if (startDateFrom && startDateTo) {
where.startDate = { [Op.between]: [new Date(startDateFrom), new Date(startDateTo)] };
}
if (endDateFrom && endDateTo) {
where.endDate = { [Op.between]: [new Date(endDateFrom), new Date(endDateTo)] };
}
// 构建包含关系
const include = [
{
model: Room,
where: {
isDeleted: 0,
...(apartmentId ? { apartmentId } : {})
},
include: [
{
model: Apartment,
where: { isDeleted: 0 }
}
]
},
{
model: Renter,
where: renterName ? { name: { [Op.like]: `%${renterName}%` } } : undefined,
required: !!renterName
}
];
// 计算偏移量
const offset = (page - 1) * pageSize;
// 查询租房数据
const { count, rows } = await Rental.findAndCountAll({
where,
include,
limit: parseInt(pageSize),
offset: parseInt(offset),
order: [['createTime', 'DESC']] // 按创建时间倒序排序
});
// 格式化数据
const formattedRentals = rows.map(formatRentalData);
// 返回结果
response.success(res, '获取成功', {
list: formattedRentals,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
} catch (error) {
response.serverError(res, '获取租房列表失败', error);
}
};
// 获取单个租房
const getRentalById = async (req, res) => {
try {
const { id } = req.params;
const rental = await Rental.findOne({
where: { id, isDeleted: 0 },
include: [
{
model: Room,
where: { isDeleted: 0 },
include: [
{
model: Apartment,
where: { isDeleted: 0 }
}
]
},
{
model: Renter
}
]
});
if (!rental) {
return response.notFound(res, '租房记录不存在');
}
const formattedRental = formatRentalData(rental);
response.success(res, '获取成功', formattedRental);
} catch (error) {
response.serverError(res, '获取租房详情失败', error);
}
};
// 创建租房
const createRental = async (req, res) => {
try {
console.log('接收到的请求数据:', req.body);
// 直接使用req.body中的数据
const body = req.body;
// 检查请求体是否存在
if (!body) {
return response.badRequest(res, '请求体不能为空');
}
// 检查所有必要参数
if (!body.roomId) {
return response.badRequest(res, '缺少房间ID');
}
if (!body.renterId) {
return response.badRequest(res, '缺少租客ID');
}
if (!body.startDate) {
return response.badRequest(res, '缺少开始日期');
}
if (!body.endDate) {
return response.badRequest(res, '缺少结束日期');
}
if (!body.rent) {
return response.badRequest(res, '缺少租金');
}
// 转换roomId和renterId为整数类型
const parsedRoomId = parseInt(body.roomId);
const parsedRenterId = parseInt(body.renterId);
if (isNaN(parsedRoomId)) {
return response.badRequest(res, '无效的房间ID');
}
if (isNaN(parsedRenterId)) {
return response.badRequest(res, '无效的租客ID');
}
// 处理押金为空时设置为0
const deposit = body.deposit || 0;
// 获取租客信息用于账单备注
const renter = await Renter.findByPk(parsedRenterId);
const renterName = renter ? renter.name : '';
// 创建租房记录
console.log('创建租房记录:', {
roomId: parsedRoomId,
renterId: parsedRenterId,
startDate: body.startDate,
endDate: body.endDate,
paymentType: body.paymentType || 'monthly',
rent: body.rent,
deposit: deposit,
operator: body.operator,
waterMeterStart: body.waterMeterStart,
electricityMeterStart: body.electricityMeterStart,
status: body.status || 'active',
remark: body.remark
});
const rental = await Rental.create({
roomId: parsedRoomId,
renterId: parsedRenterId,
startDate: body.startDate,
endDate: body.endDate,
paymentType: body.paymentType || 'monthly',
rent: body.rent,
deposit: deposit,
operator: body.operator || null,
waterMeterStart: body.waterMeterStart || null,
electricityMeterStart: body.electricityMeterStart || null,
status: body.status || 'active',
remark: body.remark,
createBy: req.user.id,
updateBy: req.user.id
});
console.log('租房记录:', rental);
// 更新房间状态为已租
await Room.update({ status: 'rented', rentalStatus: 'normal' }, { where: { id: parsedRoomId } });
// 自动生成租金账单
await Bill.create({
rentalId: rental.id,
roomId: parsedRoomId,
type: 'income',
category: 'rent',
amount: body.rent,
paidAmount: 0,
status: 'pending',
dueDate: body.endDate,
periodStart: body.startDate,
periodEnd: body.endDate,
remark: `租约租金 - ${renterName}`,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
});
// 自动生成押金账单(如果有押金)
if (deposit > 0) {
await Bill.create({
rentalId: rental.id,
roomId: parsedRoomId,
type: 'income',
category: 'deposit',
amount: deposit,
paidAmount: 0,
status: 'pending',
dueDate: body.startDate,
remark: `租约押金 - ${renterName}`,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
});
}
response.created(res, '创建成功', rental);
} catch (error) {
console.error('创建租房记录时出错:', error);
response.serverError(res, '创建租房失败', error);
}
};
// 更新租房
const updateRental = async (req, res) => {
try {
const { id } = req.params;
const { roomId, renterId, startDate, endDate, paymentType, rent, deposit, operator, waterMeterStart, electricityMeterStart, waterMeterEnd, electricityMeterEnd, status, remark } = req.body;
const rental = await Rental.findOne({
where: { id, isDeleted: 0 }
});
if (!rental) {
return response.notFound(res, '租房记录不存在');
}
// 处理押金为空时设置为0
const updateDeposit = deposit || 0;
await rental.update({
roomId,
renterId,
startDate,
endDate,
paymentType: paymentType || 'monthly',
rent,
deposit: updateDeposit,
operator: operator || null,
waterMeterStart: waterMeterStart || null,
electricityMeterStart: electricityMeterStart || null,
waterMeterEnd: waterMeterEnd || null,
electricityMeterEnd: electricityMeterEnd || null,
status,
remark,
updateBy: req.user.id
});
response.success(res, '更新成功', rental);
} catch (error) {
response.serverError(res, '更新租房失败', error);
}
};
// 删除租房(软删除)
const deleteRental = async (req, res) => {
try {
const { id } = req.params;
const rental = await Rental.findOne({
where: { id, isDeleted: 0 }
});
if (!rental) {
return response.notFound(res, '租房记录不存在');
}
await rental.update({
isDeleted: 1,
updateBy: req.user.id
});
response.success(res, '租房记录删除成功');
} catch (error) {
response.serverError(res, '删除租房失败', error);
}
};
// 退租处理
const terminateRental = async (req, res) => {
try {
const { id } = req.params;
const { waterMeterEnd, electricityMeterEnd, remark } = req.body;
const rental = await Rental.findOne({
where: { id, isDeleted: 0 },
include: [
{
model: Room,
where: { isDeleted: 0 },
include: [
{
model: Apartment,
where: { isDeleted: 0 }
}
]
},
{
model: Renter
}
]
});
if (!rental) {
return response.notFound(res, '租房记录不存在');
}
if (rental.status !== 'active') {
return response.badRequest(res, '只有生效中的租约可以退租');
}
const room = rental.Room;
const apartment = room.Apartment;
const renterName = rental.Renter ? rental.Renter.name : '';
// 更新租约信息
await rental.update({
status: 'expired',
waterMeterEnd: waterMeterEnd || null,
electricityMeterEnd: electricityMeterEnd || null,
remark: remark ? `${rental.remark || ''}\n退租备注:${remark}` : rental.remark,
updateBy: req.user.id
});
// 生成水电费账单(如果有读数差异)
if (waterMeterEnd && rental.waterMeterStart) {
const waterUsage = parseFloat(waterMeterEnd) - parseFloat(rental.waterMeterStart);
if (waterUsage > 0 && apartment.waterPrice) {
const waterAmount = waterUsage * parseFloat(apartment.waterPrice);
await Bill.create({
rentalId: rental.id,
roomId: room.id,
type: 'income',
category: 'water',
amount: waterAmount,
paidAmount: 0,
status: 'pending',
remark: `退租水费 - ${renterName}(用量:${waterUsage.toFixed(2)}吨)`,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
});
}
}
if (electricityMeterEnd && rental.electricityMeterStart) {
const electricityUsage = parseFloat(electricityMeterEnd) - parseFloat(rental.electricityMeterStart);
if (electricityUsage > 0 && apartment.electricityPrice) {
const electricityAmount = electricityUsage * parseFloat(apartment.electricityPrice);
await Bill.create({
rentalId: rental.id,
roomId: room.id,
type: 'income',
category: 'electricity',
amount: electricityAmount,
paidAmount: 0,
status: 'pending',
remark: `退租电费 - ${renterName}(用量:${electricityUsage.toFixed(2)}度)`,
tenantId: req.user.tenantId,
createBy: req.user.id,
updateBy: req.user.id
});
}
}
// 更新房间状态为空房
await Room.update(
{ status: 'empty', rentalStatus: 'normal' },
{ where: { id: room.id } }
);
response.success(res, '退租处理成功', rental);
} catch (error) {
console.error('退租处理时出错:', error);
response.serverError(res, '退租处理失败', error);
}
};
// 获取所有租房(不分页)
const listRentals = async (req, res) => {
try {
// 先检查并更新租房状态
await checkAndUpdateRentalStatus();
const {
apartmentId,
roomId,
renterName,
status,
startDateFrom,
startDateTo,
endDateFrom,
endDateTo
} = req.query;
// 构建查询条件
const where = { isDeleted: 0 };
if (status) {
where.status = status;
}
if (roomId) {
where.roomId = roomId;
}
if (startDateFrom && startDateTo) {
where.startDate = { [Op.between]: [new Date(startDateFrom), new Date(startDateTo)] };
}
if (endDateFrom && endDateTo) {
where.endDate = { [Op.between]: [new Date(endDateFrom), new Date(endDateTo)] };
}
// 构建包含关系
const include = [
{
model: Room,
where: {
isDeleted: 0,
...(apartmentId ? { apartmentId } : {})
},
include: [
{
model: Apartment,
where: { isDeleted: 0 }
}
]
},
{
model: Renter,
where: renterName ? { name: { [Op.like]: `%${renterName}%` } } : undefined,
required: !!renterName
}
];
// 查询租房数据
const rentals = await Rental.findAll({
where,
include,
order: [['createTime', 'DESC']] // 按创建时间倒序排序
});
// 格式化数据
const formattedRentals = rentals.map(formatRentalData);
// 返回结果
response.success(res, '获取成功', formattedRentals);
} catch (error) {
response.serverError(res, '获取租房列表失败', error);
}
};
// 创建租房(包含租客、水电表读数)- 整合接口
const createRentalWithRenter = async (req, res) => {
const transaction = await require('../config/db').transaction();
try {
console.log('接收到的整合请求数据:', req.body);
const {
// 租客信息
renterName,
renterPhone,
renterIdCard,
// 租房信息
roomId,
paymentType = 'monthly',
startDate,
endDate,
rent,
deposit = 0,
operator,
remark,
// 水电表读数
waterStartReading,
electricStartReading
} = req.body;
const tenantId = req.user.tenantId;
const createBy = req.user.id;
// 参数验证
if (!renterName) {
await transaction.rollback();
return response.badRequest(res, '租客姓名不能为空');
}
if (!roomId) {
await transaction.rollback();
return response.badRequest(res, '房间ID不能为空');
}
if (!startDate || !endDate) {
await transaction.rollback();
return response.badRequest(res, '开始日期和结束日期不能为空');
}
if (!rent) {
await transaction.rollback();
return response.badRequest(res, '租金不能为空');
}
const parsedRoomId = parseInt(roomId);
if (isNaN(parsedRoomId)) {
await transaction.rollback();
return response.badRequest(res, '无效的房间ID');
}
// 1. 创建租客
const renterData = {
name: renterName,
phone: renterPhone || null,
idCard: renterIdCard || null,
tenantId,
createBy,
status: 'active'
};
const renter = await Renter.create(renterData, { transaction });
console.log('租客创建成功:', renter.id);
// 2. 创建租房记录
const rentalData = {
roomId: parsedRoomId,
renterId: renter.id,
startDate,
endDate,
paymentType,
rent,
deposit,
operator: operator || null,
waterMeterStart: waterStartReading || null,
electricityMeterStart: electricStartReading || null,
status: 'active',
remark: remark || null,
tenantId,
createBy,
updateBy: createBy
};
const rental = await Rental.create(rentalData, { transaction });
console.log('租房记录创建成功:', rental.id);
// 3. 更新房间状态为已租
await Room.update(
{ status: 'rented', rentalStatus: 'normal' },
{ where: { id: parsedRoomId }, transaction }
);
// 生成账单编号
const generateBillNo = () => {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0')
return `B${year}${month}${day}${random}`
}
// 4. 创建租金账单
await Bill.create({
billNo: generateBillNo(),
rentalId: rental.id,
roomId: parsedRoomId,
renterId: renter.id,
type: 'income',
category: 'rent',
receivableAmount: rent,
receivedAmount: 0,
status: 'unpaid',
billDate: startDate,
billMonth: startDate.substring(0, 7),
remark: `租约租金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
// 5. 创建押金账单(如果有押金)
if (deposit > 0) {
await Bill.create({
billNo: generateBillNo(),
rentalId: rental.id,
roomId: parsedRoomId,
renterId: renter.id,
type: 'income',
category: 'deposit',
receivableAmount: deposit,
receivedAmount: 0,
status: 'unpaid',
billDate: startDate,
billMonth: startDate.substring(0, 7),
remark: `租约押金 - ${renterName}`,
tenantId,
createBy,
updateBy: createBy
}, { transaction });
}
// 6. 创建水表读数记录(如果有)
if (waterStartReading !== null && waterStartReading !== '' && waterStartReading !== undefined) {
// 获取公寓水费单价
const room = await Room.findByPk(parsedRoomId, {
include: [{ model: Apartment, as: 'apartment' }],
transaction
});
const waterPrice = room?.apartment?.waterPrice || 0;
await MeterReading.create({
roomId: parsedRoomId,
renterId: renter.id,
rentalId: rental.id,
meterType: 'water',
previousReading: 0,
currentReading: waterStartReading,
usage: 0,
unitPrice: waterPrice,
amount: 0,
billMonth: startDate.substring(0, 7),
readingDate: startDate,
remark: '入住时水表读数',
tenantId,
createBy,
updateBy: createBy
}, { transaction });
console.log('水表读数记录创建成功');
}
// 7. 创建电表读数记录(如果有)
if (electricStartReading !== null && electricStartReading !== '' && electricStartReading !== undefined) {
// 获取公寓电费单价
const room = await Room.findByPk(parsedRoomId, {
include: [{ model: Apartment, as: 'apartment' }],
transaction
});
const electricityPrice = room?.apartment?.electricityPrice || 0;
await MeterReading.create({
roomId: parsedRoomId,
renterId: renter.id,
rentalId: rental.id,
meterType: 'electricity',
previousReading: 0,
currentReading: electricStartReading,
usage: 0,
unitPrice: electricityPrice,
amount: 0,
billMonth: startDate.substring(0, 7),
readingDate: startDate,
remark: '入住时电表读数',
tenantId,
createBy,
updateBy: createBy
}, { transaction });
console.log('电表读数记录创建成功');
}
// 提交事务
await transaction.commit();
response.created(res, '创建成功', {
rentalId: rental.id,
renterId: renter.id,
roomId: parsedRoomId
});
} catch (error) {
// 回滚事务
await transaction.rollback();
console.error('创建租房(整合)时出错:', error);
response.serverError(res, '创建租房失败', error);
}
};
module.exports = {
getAllRentals,
listRentals,
getRentalById,
createRental,
updateRental,
deleteRental,
terminateRental,
createRentalWithRenter
};