2026-04-20 06:43:09 +00:00
|
|
|
|
const { Rental, Room, Apartment, Bill, Transaction, Renter, MeterReading } = require('../models');
|
|
|
|
|
|
const { Op } = require('sequelize');
|
2026-04-22 06:48:32 +00:00
|
|
|
|
const response = require('../utils/response');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
// 格式化时间(考虑时区,转换为北京时间)
|
|
|
|
|
|
const formatDate = (date) => {
|
|
|
|
|
|
if (!date) return null;
|
|
|
|
|
|
const dateObj = date instanceof Date ? date : new Date(date);
|
|
|
|
|
|
if (isNaN(dateObj.getTime())) return null;
|
|
|
|
|
|
const beijingDate = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000);
|
|
|
|
|
|
return beijingDate.toISOString().split('T')[0];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-09 09:01:41 +00:00
|
|
|
|
// 格式化日期时间(年月日时分秒)
|
|
|
|
|
|
const formatDateTime = (date) => {
|
|
|
|
|
|
if (!date) return null;
|
|
|
|
|
|
const dateObj = date instanceof Date ? date : new Date(date);
|
|
|
|
|
|
if (isNaN(dateObj.getTime())) return null;
|
|
|
|
|
|
const beijingDate = new Date(dateObj.getTime() + 8 * 60 * 60 * 1000);
|
|
|
|
|
|
return beijingDate.toISOString().replace('T', ' ').slice(0, 19);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
// 格式化租房数据
|
|
|
|
|
|
const formatRentalData = (rental) => {
|
|
|
|
|
|
const formattedRental = {
|
|
|
|
|
|
...rental.toJSON(),
|
|
|
|
|
|
startDate: formatDate(rental.startDate),
|
|
|
|
|
|
endDate: formatDate(rental.endDate),
|
2026-05-09 09:01:41 +00:00
|
|
|
|
createTime: formatDateTime(rental.createTime),
|
|
|
|
|
|
updateTime: formatDateTime(rental.updateTime)
|
2026-04-20 06:43:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return formattedRental;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 检查并更新租房状态
|
2026-05-09 09:01:41 +00:00
|
|
|
|
const checkAndUpdateRentalStatus = async (tenantId) => {
|
2026-04-20 06:43:09 +00:00
|
|
|
|
try {
|
|
|
|
|
|
// 获取当前日期
|
|
|
|
|
|
const currentDate = new Date();
|
|
|
|
|
|
// 计算5天后的日期
|
|
|
|
|
|
const fiveDaysLater = new Date();
|
|
|
|
|
|
fiveDaysLater.setDate(currentDate.getDate() + 5);
|
|
|
|
|
|
|
2026-05-09 09:01:41 +00:00
|
|
|
|
// 查找该租户下所有活跃的租房记录
|
2026-04-20 06:43:09 +00:00
|
|
|
|
const rentals = await Rental.findAll({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
where: { tenantId, status: 'active', isDeleted: 0 },
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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 {
|
|
|
|
|
|
// 先检查并更新租房状态
|
2026-05-09 09:01:41 +00:00
|
|
|
|
await checkAndUpdateRentalStatus(req.user.tenantId);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
apartmentId,
|
|
|
|
|
|
roomId,
|
|
|
|
|
|
renterName,
|
|
|
|
|
|
status,
|
|
|
|
|
|
startDateFrom,
|
|
|
|
|
|
startDateTo,
|
|
|
|
|
|
endDateFrom,
|
|
|
|
|
|
endDateTo,
|
|
|
|
|
|
page = 1,
|
|
|
|
|
|
pageSize = 10
|
|
|
|
|
|
} = req.query;
|
|
|
|
|
|
|
|
|
|
|
|
// 构建查询条件
|
2026-05-09 09:01:41 +00:00
|
|
|
|
const where = { tenantId: req.user.tenantId, isDeleted: 0 };
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 返回结果
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.success(res, '获取成功', {
|
|
|
|
|
|
list: formattedRentals,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
total: count,
|
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
|
pageSize: parseInt(pageSize)
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '获取租房列表失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取单个租房
|
|
|
|
|
|
const getRentalById = async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const rental = await Rental.findOne({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
|
2026-04-20 06:43:09 +00:00
|
|
|
|
include: [
|
|
|
|
|
|
{
|
|
|
|
|
|
model: Room,
|
|
|
|
|
|
where: { isDeleted: 0 },
|
|
|
|
|
|
include: [
|
|
|
|
|
|
{
|
|
|
|
|
|
model: Apartment,
|
|
|
|
|
|
where: { isDeleted: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
model: Renter
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!rental) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.notFound(res, '租房记录不存在');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
const formattedRental = formatRentalData(rental);
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.success(res, '获取成功', formattedRental);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '获取租房详情失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 创建租房
|
|
|
|
|
|
const createRental = async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('接收到的请求数据:', req.body);
|
|
|
|
|
|
|
|
|
|
|
|
// 直接使用req.body中的数据
|
|
|
|
|
|
const body = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查请求体是否存在
|
|
|
|
|
|
if (!body) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '请求体不能为空');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查所有必要参数
|
|
|
|
|
|
if (!body.roomId) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '缺少房间ID');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!body.renterId) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '缺少租客ID');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!body.startDate) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '缺少开始日期');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!body.endDate) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '缺少结束日期');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!body.rent) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '缺少租金');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 转换roomId和renterId为整数类型
|
|
|
|
|
|
const parsedRoomId = parseInt(body.roomId);
|
|
|
|
|
|
const parsedRenterId = parseInt(body.renterId);
|
|
|
|
|
|
if (isNaN(parsedRoomId)) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '无效的房间ID');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (isNaN(parsedRenterId)) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '无效的租客ID');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理押金,为空时设置为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,
|
2026-05-09 09:01:41 +00:00
|
|
|
|
tenantId: req.user.tenantId,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
createBy: req.user.id,
|
|
|
|
|
|
updateBy: req.user.id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('租房记录:', rental);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新房间状态为已租
|
|
|
|
|
|
await Room.update({ status: 'rented', rentalStatus: 'normal' }, { where: { id: parsedRoomId } });
|
|
|
|
|
|
|
|
|
|
|
|
// 自动生成租金账单
|
|
|
|
|
|
await Bill.create({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
billNo: 'B' + Date.now(),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
roomId: parsedRoomId,
|
2026-05-09 09:01:41 +00:00
|
|
|
|
renterId: parsedRenterId,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'rent',
|
2026-05-09 09:01:41 +00:00
|
|
|
|
receivableAmount: body.rent,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: body.startDate,
|
|
|
|
|
|
billMonth: body.startDate.substring(0, 7),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
remark: `租约租金 - ${renterName}`,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
createBy: req.user.id,
|
|
|
|
|
|
updateBy: req.user.id
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 自动生成押金账单(如果有押金)
|
|
|
|
|
|
if (deposit > 0) {
|
|
|
|
|
|
await Bill.create({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
billNo: 'B' + Date.now() + '1',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
roomId: parsedRoomId,
|
2026-05-09 09:01:41 +00:00
|
|
|
|
renterId: parsedRenterId,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'deposit',
|
2026-05-09 09:01:41 +00:00
|
|
|
|
receivableAmount: deposit,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: body.startDate,
|
|
|
|
|
|
billMonth: body.startDate.substring(0, 7),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
remark: `租约押金 - ${renterName}`,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
createBy: req.user.id,
|
|
|
|
|
|
updateBy: req.user.id
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.created(res, '创建成功', rental);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('创建租房记录时出错:', error);
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '创建租房失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新租房
|
|
|
|
|
|
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({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
where: { id, tenantId: req.user.tenantId, isDeleted: 0 }
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
|
|
|
|
|
if (!rental) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.notFound(res, '租房记录不存在');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
// 处理押金,为空时设置为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
|
|
|
|
|
|
});
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.success(res, '更新成功', rental);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '更新租房失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 删除租房(软删除)
|
|
|
|
|
|
const deleteRental = async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const rental = await Rental.findOne({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
where: { id, tenantId: req.user.tenantId, isDeleted: 0 }
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
|
|
|
|
|
if (!rental) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.notFound(res, '租房记录不存在');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
await rental.update({
|
|
|
|
|
|
isDeleted: 1,
|
|
|
|
|
|
updateBy: req.user.id
|
|
|
|
|
|
});
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.success(res, '租房记录删除成功');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '删除租房失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 退租处理
|
|
|
|
|
|
const terminateRental = async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const { waterMeterEnd, electricityMeterEnd, remark } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
const rental = await Rental.findOne({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
|
2026-04-20 06:43:09 +00:00
|
|
|
|
include: [
|
|
|
|
|
|
{
|
|
|
|
|
|
model: Room,
|
|
|
|
|
|
where: { isDeleted: 0 },
|
|
|
|
|
|
include: [
|
|
|
|
|
|
{
|
|
|
|
|
|
model: Apartment,
|
|
|
|
|
|
where: { isDeleted: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
model: Renter
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!rental) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.notFound(res, '租房记录不存在');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rental.status !== 'active') {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '只有生效中的租约可以退租');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
billNo: 'B' + Date.now(),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
roomId: room.id,
|
2026-05-09 09:01:41 +00:00
|
|
|
|
renterId: rental.renterId,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'water',
|
2026-05-09 09:01:41 +00:00
|
|
|
|
receivableAmount: waterAmount,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: new Date(),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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({
|
2026-05-09 09:01:41 +00:00
|
|
|
|
billNo: 'B' + Date.now() + '1',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
roomId: room.id,
|
2026-05-09 09:01:41 +00:00
|
|
|
|
renterId: rental.renterId,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'electricity',
|
2026-05-09 09:01:41 +00:00
|
|
|
|
receivableAmount: electricityAmount,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: new Date(),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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 } }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.success(res, '退租处理成功', rental);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('退租处理时出错:', error);
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '退租处理失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有租房(不分页)
|
|
|
|
|
|
const listRentals = async (req, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先检查并更新租房状态
|
2026-05-09 09:01:41 +00:00
|
|
|
|
await checkAndUpdateRentalStatus(req.user.tenantId);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
apartmentId,
|
|
|
|
|
|
roomId,
|
|
|
|
|
|
renterName,
|
|
|
|
|
|
status,
|
|
|
|
|
|
startDateFrom,
|
|
|
|
|
|
startDateTo,
|
|
|
|
|
|
endDateFrom,
|
|
|
|
|
|
endDateTo
|
|
|
|
|
|
} = req.query;
|
|
|
|
|
|
|
|
|
|
|
|
// 构建查询条件
|
2026-05-09 09:01:41 +00:00
|
|
|
|
const where = { tenantId: req.user.tenantId, isDeleted: 0 };
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 返回结果
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.success(res, '获取成功', formattedRentals);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '获取租房列表失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 创建租房(包含租客、水电表读数)- 整合接口
|
|
|
|
|
|
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();
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '租客姓名不能为空');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!roomId) {
|
|
|
|
|
|
await transaction.rollback();
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '房间ID不能为空');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!startDate || !endDate) {
|
|
|
|
|
|
await transaction.rollback();
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '开始日期和结束日期不能为空');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (!rent) {
|
|
|
|
|
|
await transaction.rollback();
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '租金不能为空');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parsedRoomId = parseInt(roomId);
|
|
|
|
|
|
if (isNaN(parsedRoomId)) {
|
|
|
|
|
|
await transaction.rollback();
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '无效的房间ID');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.created(res, '创建成功', {
|
|
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
renterId: renter.id,
|
|
|
|
|
|
roomId: parsedRoomId
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// 回滚事务
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
console.error('创建租房(整合)时出错:', error);
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '创建租房失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
getAllRentals,
|
|
|
|
|
|
listRentals,
|
|
|
|
|
|
getRentalById,
|
|
|
|
|
|
createRental,
|
|
|
|
|
|
updateRental,
|
|
|
|
|
|
deleteRental,
|
|
|
|
|
|
terminateRental,
|
|
|
|
|
|
createRentalWithRenter
|
|
|
|
|
|
};
|