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-06-11 13:45:33 +00:00
|
|
|
|
const rentalBillingService = require('../services/rentalBillingService');
|
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-06-11 13:45:33 +00:00
|
|
|
|
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}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const buildBillingMeta = (settlementType, sourceEventId = null) => ({
|
|
|
|
|
|
settlementType,
|
|
|
|
|
|
sourceEventId
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
// 检查并更新租房状态
|
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: [
|
|
|
|
|
|
{
|
2026-06-11 13:45:33 +00:00
|
|
|
|
model: Apartment
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 检查房间是否存在
|
|
|
|
|
|
const room = await Room.findByPk(parsedRoomId);
|
|
|
|
|
|
if (!room) {
|
|
|
|
|
|
return response.badRequest(res, '房间不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查房间是否有未结束的租赁合同
|
|
|
|
|
|
const activeRental = await Rental.findOne({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
roomId: parsedRoomId,
|
|
|
|
|
|
status: { [Op.in]: ['active', 'reserved'] },
|
|
|
|
|
|
endDate: { [Op.gte]: new Date() }
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
if (activeRental) {
|
|
|
|
|
|
return response.badRequest(res, '该房间已有进行中的租赁合同');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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 : '';
|
2026-06-11 13:45:33 +00:00
|
|
|
|
var transaction;
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
// 创建租房记录
|
|
|
|
|
|
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
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
transaction = await require('../config/db').transaction();
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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
|
2026-06-11 13:45:33 +00:00
|
|
|
|
}, { transaction });
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
console.log('租房记录:', rental);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新房间状态为已租
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await Room.update({ status: 'rented', rentalStatus: 'normal' }, { where: { id: parsedRoomId }, transaction });
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
// 自动生成租金账单
|
|
|
|
|
|
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-06-11 13:45:33 +00:00
|
|
|
|
settlementType: 'normal',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
remark: `租约租金 - ${renterName}`,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
createBy: req.user.id,
|
|
|
|
|
|
updateBy: req.user.id
|
2026-06-11 13:45:33 +00:00
|
|
|
|
}, { transaction });
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
// 自动生成押金账单(如果有押金)
|
|
|
|
|
|
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-06-11 13:45:33 +00:00
|
|
|
|
settlementType: 'normal',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
remark: `租约押金 - ${renterName}`,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
createBy: req.user.id,
|
|
|
|
|
|
updateBy: req.user.id
|
2026-06-11 13:45:33 +00:00
|
|
|
|
}, { transaction });
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await transaction.commit();
|
|
|
|
|
|
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.created(res, '创建成功', rental);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
if (typeof transaction !== 'undefined' && transaction) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
}
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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;
|
2026-06-11 13:45:33 +00:00
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
const rental = await Rental.findOne({
|
2026-06-11 13:45:33 +00:00
|
|
|
|
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
|
|
|
|
|
|
include: [{ model: Room }]
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
2026-06-11 13:45:33 +00:00
|
|
|
|
|
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
|
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
|
|
|
|
|
|
|
// 状态校验:已终止/已到期的租约不能编辑
|
|
|
|
|
|
if (rental.status === 'terminated' || rental.status === 'expired') {
|
|
|
|
|
|
return response.badRequest(res, '已终止或已到期的租约不能编辑');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const oldRental = rental.toJSON();
|
|
|
|
|
|
var transaction = await require('../config/db').transaction();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果更换了房间,检查新房间是否有效
|
|
|
|
|
|
if (roomId && roomId !== rental.roomId) {
|
|
|
|
|
|
const newRoom = await Room.findOne({
|
|
|
|
|
|
where: { id: roomId, tenantId: req.user.tenantId, isDeleted: 0 }
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!newRoom) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, '新房间不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newRoom.status === 'rented') {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, '新房间已在租');
|
|
|
|
|
|
}
|
|
|
|
|
|
// 更新旧房间状态为空房
|
|
|
|
|
|
await Room.update(
|
|
|
|
|
|
{ status: 'empty', rentalStatus: 'normal' },
|
|
|
|
|
|
{ where: { id: rental.roomId }, transaction }
|
|
|
|
|
|
);
|
|
|
|
|
|
// 更新新房间状态为已租
|
|
|
|
|
|
await Room.update(
|
|
|
|
|
|
{ status: 'rented', rentalStatus: 'normal' },
|
|
|
|
|
|
{ where: { id: roomId }, transaction }
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-06-11 13:45:33 +00:00
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
|
|
const hasPaidBills = await Bill.count({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
status: { [Op.in]: ['paid', 'partial'] },
|
|
|
|
|
|
isDeleted: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
transaction
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
2026-06-11 13:45:33 +00:00
|
|
|
|
const financialChanged = String(oldRental.rent) !== String(rental.rent) || String(oldRental.deposit || 0) !== String(rental.deposit || 0) || String(oldRental.startDate) !== String(rental.startDate) || oldRental.roomId !== rental.roomId || oldRental.renterId !== rental.renterId;
|
|
|
|
|
|
if (hasPaidBills > 0 && financialChanged) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, '该租约已有收款记录,不能直接修改租金、押金、日期、房间或租客');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renter = await Renter.findByPk(rental.renterId, { transaction });
|
|
|
|
|
|
await rentalBillingService.syncEditableInitialBills({
|
|
|
|
|
|
rental,
|
|
|
|
|
|
oldRental,
|
|
|
|
|
|
renterName: renter ? renter.name : '',
|
|
|
|
|
|
operatorId: req.user.id,
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await transaction.commit();
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.success(res, '更新成功', rental);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
if (typeof transaction !== 'undefined' && transaction) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
}
|
|
|
|
|
|
response.serverError(res, '更新租约失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 删除租约(软删除)
|
2026-04-20 06:43:09 +00:00
|
|
|
|
const deleteRental = async (req, res) => {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
const transaction = await require('../config/db').transaction();
|
2026-04-20 06:43:09 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const rental = await Rental.findOne({
|
2026-06-11 13:45:33 +00:00
|
|
|
|
where: { id, tenantId: req.user.tenantId, isDeleted: 0 },
|
|
|
|
|
|
transaction
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
|
|
|
|
|
if (!rental) {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.notFound(res, '租约不存在');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
|
|
|
|
|
|
|
await rentalBillingService.assertRentalCanBeDeleted({
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await rentalBillingService.cancelUnpaidBillsForRental({
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
operatorId: req.user.id,
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
await rental.update({
|
|
|
|
|
|
isDeleted: 1,
|
|
|
|
|
|
updateBy: req.user.id
|
2026-06-11 13:45:33 +00:00
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
|
|
const otherActiveRental = await Rental.findOne({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
id: { [Op.ne]: id },
|
|
|
|
|
|
roomId: rental.roomId,
|
|
|
|
|
|
tenantId: req.user.tenantId,
|
|
|
|
|
|
status: 'active',
|
|
|
|
|
|
isDeleted: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
transaction
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
2026-06-11 13:45:33 +00:00
|
|
|
|
|
|
|
|
|
|
if (!otherActiveRental) {
|
|
|
|
|
|
await Room.update(
|
|
|
|
|
|
{ status: 'empty', rentalStatus: 'normal' },
|
|
|
|
|
|
{ where: { id: rental.roomId, tenantId: req.user.tenantId }, transaction }
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await transaction.commit();
|
|
|
|
|
|
response.success(res, '租约删除成功');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
if (error.statusCode === 400) {
|
|
|
|
|
|
return response.badRequest(res, error.message);
|
|
|
|
|
|
}
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '删除租房失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 退租处理(优化版:结算水电费 + 退还押金)
|
2026-04-20 06:43:09 +00:00
|
|
|
|
const terminateRental = async (req, res) => {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
const transaction = await require('../config/db').transaction();
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
2026-06-11 13:45:33 +00:00
|
|
|
|
const {
|
|
|
|
|
|
waterMeterEnd,
|
|
|
|
|
|
electricityMeterEnd,
|
|
|
|
|
|
refundDeposit,
|
|
|
|
|
|
remark
|
|
|
|
|
|
} = req.body;
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
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 },
|
2026-06-11 13:45:33 +00:00
|
|
|
|
include: [{ model: Apartment, where: { isDeleted: 0 } }]
|
2026-04-20 06:43:09 +00:00
|
|
|
|
},
|
2026-06-11 13:45:33 +00:00
|
|
|
|
{ model: Renter }
|
|
|
|
|
|
],
|
|
|
|
|
|
transaction
|
2026-04-20 06:43:09 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!rental) {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.notFound(res, '租约不存在');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rental.status !== 'active') {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await transaction.rollback();
|
2026-04-22 06:48:32 +00:00
|
|
|
|
return response.badRequest(res, '只有生效中的租约可以退租');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 检查是否有未支付的租金/水电费账单
|
|
|
|
|
|
const unpaidBills = await Bill.findAll({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
status: { [Op.in]: ['unpaid', 'partial'] },
|
|
|
|
|
|
category: { [Op.in]: ['rent', 'water', 'electricity'] },
|
|
|
|
|
|
isDeleted: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (unpaidBills && unpaidBills.length > 0) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, `该租约有 ${unpaidBills.length} 笔未结清的账单,请先结清后再退租`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
const room = rental.Room;
|
|
|
|
|
|
const apartment = room.Apartment;
|
|
|
|
|
|
const renterName = rental.Renter ? rental.Renter.name : '';
|
2026-06-11 13:45:33 +00:00
|
|
|
|
const createBy = req.user.id;
|
|
|
|
|
|
const tenantId = req.user.tenantId;
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
|
|
|
|
|
|
const billsGenerated = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 更新租约信息
|
2026-04-20 06:43:09 +00:00
|
|
|
|
await rental.update({
|
2026-06-11 13:45:33 +00:00
|
|
|
|
status: 'terminated',
|
|
|
|
|
|
closeReason: 'terminated',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
waterMeterEnd: waterMeterEnd || null,
|
|
|
|
|
|
electricityMeterEnd: electricityMeterEnd || null,
|
|
|
|
|
|
remark: remark ? `${rental.remark || ''}\n退租备注:${remark}` : rental.remark,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
2026-04-20 06:43:09 +00:00
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 2. 生成水电费账单(如有读数差异)
|
2026-04-20 06:43:09 +00:00
|
|
|
|
if (waterMeterEnd && rental.waterMeterStart) {
|
|
|
|
|
|
const waterUsage = parseFloat(waterMeterEnd) - parseFloat(rental.waterMeterStart);
|
2026-06-11 13:45:33 +00:00
|
|
|
|
if (waterUsage > 0) {
|
|
|
|
|
|
const waterAmount = waterUsage * parseFloat(apartment.waterPrice || 0);
|
|
|
|
|
|
const bill = await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: id,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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-06-11 13:45:33 +00:00
|
|
|
|
settlementType: 'checkout',
|
|
|
|
|
|
sourceEventId: id,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
remark: `退租水费 - ${renterName}(用量:${waterUsage.toFixed(2)}吨)`,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
billsGenerated.push({ category: 'water', amount: waterAmount });
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (electricityMeterEnd && rental.electricityMeterStart) {
|
|
|
|
|
|
const electricityUsage = parseFloat(electricityMeterEnd) - parseFloat(rental.electricityMeterStart);
|
2026-06-11 13:45:33 +00:00
|
|
|
|
if (electricityUsage > 0) {
|
|
|
|
|
|
const electricityAmount = electricityUsage * parseFloat(apartment.electricityPrice || 0);
|
|
|
|
|
|
const bill = await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: id,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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-06-11 13:45:33 +00:00
|
|
|
|
settlementType: 'checkout',
|
|
|
|
|
|
sourceEventId: id,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
remark: `退租电费 - ${renterName}(用量:${electricityUsage.toFixed(2)}度)`,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
billsGenerated.push({ category: 'electricity', amount: electricityAmount });
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 3. 生成押金退还账单(支出类型)
|
|
|
|
|
|
const depositToRefund = refundDeposit !== undefined ? refundDeposit : (rental.deposit || 0);
|
|
|
|
|
|
if (depositToRefund > 0) {
|
|
|
|
|
|
const bill = await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
roomId: room.id,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
type: 'expense',
|
|
|
|
|
|
category: 'deposit',
|
|
|
|
|
|
receivableAmount: depositToRefund,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: { [Op.in]: ['unpaid', 'partial'] },
|
|
|
|
|
|
billDate: new Date(),
|
|
|
|
|
|
settlementType: 'checkout',
|
|
|
|
|
|
sourceEventId: id,
|
|
|
|
|
|
remark: `退租押金退还 - ${renterName}`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
billsGenerated.push({ category: 'deposit_refund', amount: depositToRefund });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 更新房间状态为空房
|
2026-04-20 06:43:09 +00:00
|
|
|
|
await Room.update(
|
|
|
|
|
|
{ status: 'empty', rentalStatus: 'normal' },
|
2026-06-11 13:45:33 +00:00
|
|
|
|
{ where: { id: room.id }, transaction }
|
2026-04-20 06:43:09 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await transaction.commit();
|
|
|
|
|
|
|
|
|
|
|
|
response.success(res, '退租办理成功', {
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
roomId: room.id,
|
|
|
|
|
|
billsGenerated,
|
|
|
|
|
|
totalWaterBill: billsGenerated.filter(b => b.category === 'water').reduce((sum, b) => sum + b.amount, 0),
|
|
|
|
|
|
totalElectricityBill: billsGenerated.filter(b => b.category === 'electricity').reduce((sum, b) => sum + b.amount, 0),
|
|
|
|
|
|
depositRefund: depositToRefund
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
} catch (error) {
|
2026-06-11 13:45:33 +00:00
|
|
|
|
await transaction.rollback();
|
2026-04-20 06:43:09 +00:00
|
|
|
|
console.error('退租处理时出错:', error);
|
2026-04-22 06:48:32 +00:00
|
|
|
|
response.serverError(res, '退租处理失败', error);
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 续租接口
|
|
|
|
|
|
const renewRental = async (req, res) => {
|
|
|
|
|
|
const transaction = await require('../config/db').transaction();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const {
|
|
|
|
|
|
newEndDate,
|
|
|
|
|
|
newRent,
|
|
|
|
|
|
newDeposit,
|
|
|
|
|
|
paymentType = 'monthly',
|
|
|
|
|
|
startDate,
|
|
|
|
|
|
operator,
|
|
|
|
|
|
remark,
|
|
|
|
|
|
waterStartReading,
|
|
|
|
|
|
electricStartReading
|
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
const tenantId = req.user.tenantId;
|
|
|
|
|
|
const createBy = req.user.id;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取原租约信息
|
|
|
|
|
|
const rental = await Rental.findOne({
|
|
|
|
|
|
where: { id, tenantId, isDeleted: 0 },
|
|
|
|
|
|
include: [
|
|
|
|
|
|
{ model: Room, where: { isDeleted: 0 } },
|
|
|
|
|
|
{ model: Renter }
|
|
|
|
|
|
],
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!rental) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.notFound(res, '租约不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rental.status !== 'active') {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, '只有生效中的租约可以续租');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有未支付的租金/水电费账单
|
|
|
|
|
|
const unpaidBills = await Bill.findAll({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
category: { [Op.in]: ['rent', 'water', 'electricity'] },
|
|
|
|
|
|
isDeleted: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (unpaidBills && unpaidBills.length > 0) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, `该租约有 ${unpaidBills.length} 笔未结清的账单,请先结清后再续租`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renterName = rental.Renter ? rental.Renter.name : '';
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 更新原租约为已到期
|
|
|
|
|
|
await rental.update({
|
|
|
|
|
|
status: 'expired',
|
|
|
|
|
|
closeReason: 'renewed',
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 创建新租约
|
|
|
|
|
|
const newRental = await Rental.create({
|
|
|
|
|
|
roomId: rental.roomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
startDate: startDate || rental.endDate,
|
|
|
|
|
|
endDate: newEndDate,
|
|
|
|
|
|
paymentType,
|
|
|
|
|
|
rent: newRent || rental.rent,
|
|
|
|
|
|
deposit: newDeposit !== undefined ? newDeposit : rental.deposit,
|
|
|
|
|
|
operator: operator || null,
|
|
|
|
|
|
waterMeterStart: waterStartReading || null,
|
|
|
|
|
|
electricityMeterStart: electricStartReading || null,
|
|
|
|
|
|
status: 'active',
|
|
|
|
|
|
prevRentalId: rental.id,
|
|
|
|
|
|
originRentalId: rental.originRentalId || rental.id,
|
|
|
|
|
|
remark: remark || `续租自原租约${rental.id}`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 更新房间状态
|
|
|
|
|
|
await Room.update(
|
|
|
|
|
|
{ status: 'rented', rentalStatus: 'normal' },
|
|
|
|
|
|
{ where: { id: rental.roomId }, transaction }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 生成首期租金账单
|
|
|
|
|
|
const billStartDate = startDate || rental.endDate;
|
|
|
|
|
|
const billMonth = billStartDate.substring(0, 7);
|
|
|
|
|
|
|
|
|
|
|
|
await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: newRental.id,
|
|
|
|
|
|
roomId: rental.roomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'rent',
|
|
|
|
|
|
receivableAmount: newRent || rental.rent,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: billStartDate,
|
|
|
|
|
|
billMonth: billMonth,
|
|
|
|
|
|
...buildBillingMeta('renew', rental.id),
|
|
|
|
|
|
remark: `续租首期租金 - ${renterName}`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 如有押金变更,生成押金调整账单
|
|
|
|
|
|
const depositDiff = (newDeposit !== undefined ? newDeposit : rental.deposit) - rental.deposit;
|
|
|
|
|
|
if (depositDiff !== 0) {
|
|
|
|
|
|
await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: newRental.id,
|
|
|
|
|
|
roomId: rental.roomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
type: depositDiff > 0 ? 'income' : 'expense',
|
|
|
|
|
|
category: 'deposit',
|
|
|
|
|
|
receivableAmount: Math.abs(depositDiff),
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: billStartDate,
|
|
|
|
|
|
billMonth: billMonth,
|
|
|
|
|
|
...buildBillingMeta('renew', rental.id),
|
|
|
|
|
|
remark: depositDiff > 0 ? `续租补交押金 - ${renterName}` : `续租退还押金 - ${renterName}`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 记录新的水电表起始读数(如有)
|
|
|
|
|
|
if (waterStartReading !== null && waterStartReading !== undefined) {
|
|
|
|
|
|
await MeterReading.create({
|
|
|
|
|
|
roomId: rental.roomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
rentalId: newRental.id,
|
|
|
|
|
|
meterType: 'water',
|
|
|
|
|
|
previousReading: 0,
|
|
|
|
|
|
currentReading: parseFloat(waterStartReading),
|
|
|
|
|
|
usage: 0,
|
|
|
|
|
|
unitPrice: 0,
|
|
|
|
|
|
amount: 0,
|
|
|
|
|
|
billMonth: billMonth,
|
|
|
|
|
|
readingDate: billStartDate,
|
|
|
|
|
|
remark: '续租时水表读数(起始读数)',
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (electricStartReading !== null && electricStartReading !== undefined) {
|
|
|
|
|
|
await MeterReading.create({
|
|
|
|
|
|
roomId: rental.roomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
rentalId: newRental.id,
|
|
|
|
|
|
meterType: 'electricity',
|
|
|
|
|
|
previousReading: 0,
|
|
|
|
|
|
currentReading: parseFloat(electricStartReading),
|
|
|
|
|
|
usage: 0,
|
|
|
|
|
|
unitPrice: 0,
|
|
|
|
|
|
amount: 0,
|
|
|
|
|
|
billMonth: billMonth,
|
|
|
|
|
|
readingDate: billStartDate,
|
|
|
|
|
|
remark: '续租时电表读数(起始读数)',
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await transaction.commit();
|
|
|
|
|
|
|
|
|
|
|
|
response.created(res, '续租办理成功', {
|
|
|
|
|
|
oldRentalId: rental.id,
|
|
|
|
|
|
newRentalId: newRental.id,
|
|
|
|
|
|
roomId: rental.roomId,
|
|
|
|
|
|
newEndDate: newEndDate,
|
|
|
|
|
|
newRent: newRent || rental.rent
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
console.error('续租处理时出错:', error);
|
|
|
|
|
|
response.serverError(res, '续租处理失败', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 换房接口
|
|
|
|
|
|
const changeRoom = async (req, res) => {
|
|
|
|
|
|
const transaction = await require('../config/db').transaction();
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { id } = req.params;
|
|
|
|
|
|
const {
|
|
|
|
|
|
newRoomId,
|
|
|
|
|
|
changeDate,
|
|
|
|
|
|
newRent,
|
|
|
|
|
|
newDeposit,
|
|
|
|
|
|
operator,
|
|
|
|
|
|
remark,
|
|
|
|
|
|
waterMeterEnd,
|
|
|
|
|
|
electricityMeterEnd
|
|
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
const tenantId = req.user.tenantId;
|
|
|
|
|
|
const createBy = req.user.id;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取原租约信息
|
|
|
|
|
|
const rental = await Rental.findOne({
|
|
|
|
|
|
where: { id, tenantId, isDeleted: 0 },
|
|
|
|
|
|
include: [
|
|
|
|
|
|
{ model: Room, where: { isDeleted: 0 } },
|
|
|
|
|
|
{ model: Renter }
|
|
|
|
|
|
],
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!rental) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.notFound(res, '租约不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rental.status !== 'active') {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, '只有生效中的租约可以换房');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const unsettledBills = await Bill.findAll({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
status: { [Op.in]: ['unpaid', 'partial'] },
|
|
|
|
|
|
category: { [Op.in]: ['rent', 'water', 'electricity'] },
|
|
|
|
|
|
isDeleted: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (unsettledBills && unsettledBills.length > 0) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, `该租约有 ${unsettledBills.length} 笔未结清的账单,请先结清后再换房`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查新房间
|
|
|
|
|
|
const newRoom = await Room.findOne({
|
|
|
|
|
|
where: { id: newRoomId, tenantId, isDeleted: 0 },
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!newRoom) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.notFound(res, '新房间不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (newRoom.status === 'rented') {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.badRequest(res, '新房间已在租,请选择其他房间');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renterName = rental.Renter ? rental.Renter.name : '';
|
|
|
|
|
|
const oldRoomId = rental.roomId;
|
|
|
|
|
|
|
|
|
|
|
|
// 生成账单编号
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 结算原房间的水电费
|
|
|
|
|
|
if ((waterMeterEnd || electricityMeterEnd) && changeDate) {
|
|
|
|
|
|
// 获取原房间最新的抄表记录
|
|
|
|
|
|
if (waterMeterEnd) {
|
|
|
|
|
|
const waterReading = await MeterReading.findOne({
|
|
|
|
|
|
where: { roomId: oldRoomId, meterType: 'water', rentalId: id, isDeleted: 0 },
|
|
|
|
|
|
order: [['createTime', 'DESC']],
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (waterReading && waterMeterEnd > waterReading.currentReading) {
|
|
|
|
|
|
const waterUsage = waterMeterEnd - waterReading.currentReading;
|
|
|
|
|
|
const waterAmount = waterUsage * parseFloat(waterReading.unitPrice || 0);
|
|
|
|
|
|
|
|
|
|
|
|
await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
roomId: oldRoomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'water',
|
|
|
|
|
|
receivableAmount: waterAmount,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: changeDate,
|
|
|
|
|
|
billMonth: changeDate.substring(0, 7),
|
|
|
|
|
|
...buildBillingMeta('change_room', rental.id),
|
|
|
|
|
|
remark: `换房结算水费(${renterName},用量:${waterUsage.toFixed(2)}吨)`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (electricityMeterEnd) {
|
|
|
|
|
|
const electricityReading = await MeterReading.findOne({
|
|
|
|
|
|
where: { roomId: oldRoomId, meterType: 'electricity', rentalId: id, isDeleted: 0 },
|
|
|
|
|
|
order: [['createTime', 'DESC']],
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (electricityReading && electricityMeterEnd > electricityReading.currentReading) {
|
|
|
|
|
|
const electricityUsage = electricityMeterEnd - electricityReading.currentReading;
|
|
|
|
|
|
const electricityAmount = electricityUsage * parseFloat(electricityReading.unitPrice || 0);
|
|
|
|
|
|
|
|
|
|
|
|
await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: id,
|
|
|
|
|
|
roomId: oldRoomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'electricity',
|
|
|
|
|
|
receivableAmount: electricityAmount,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: changeDate,
|
|
|
|
|
|
billMonth: changeDate.substring(0, 7),
|
|
|
|
|
|
...buildBillingMeta('change_room', rental.id),
|
|
|
|
|
|
remark: `换房结算电费(${renterName},用量:${electricityUsage.toFixed(2)}度)`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 更新原房间状态为空房
|
|
|
|
|
|
await Room.update(
|
|
|
|
|
|
{ status: 'empty', rentalStatus: 'normal' },
|
|
|
|
|
|
{ where: { id: oldRoomId }, transaction }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 关闭原租约(关旧)
|
|
|
|
|
|
await rental.update({
|
|
|
|
|
|
status: 'terminated',
|
|
|
|
|
|
closeReason: 'changed_room',
|
|
|
|
|
|
remark: `${rental.remark || ''}${remark ? `\n换房备注:${remark}` : ''}`.trim(),
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 新建租约(开新)
|
|
|
|
|
|
const newRental = await Rental.create({
|
|
|
|
|
|
roomId: newRoomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
startDate: changeDate || new Date(),
|
|
|
|
|
|
endDate: rental.endDate,
|
|
|
|
|
|
paymentType: rental.paymentType,
|
|
|
|
|
|
rent: newRent || rental.rent,
|
|
|
|
|
|
deposit: newDeposit !== undefined ? newDeposit : rental.deposit,
|
|
|
|
|
|
operator: operator || rental.operator,
|
|
|
|
|
|
waterMeterStart: waterMeterEnd || null,
|
|
|
|
|
|
electricityMeterStart: electricityMeterEnd || null,
|
|
|
|
|
|
status: 'active',
|
|
|
|
|
|
prevRentalId: rental.id,
|
|
|
|
|
|
originRentalId: rental.originRentalId || rental.id,
|
|
|
|
|
|
remark: `由租约${rental.id}换房新建`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 生成新房间租金账单(如有租金)
|
|
|
|
|
|
const newRoomRent = newRent || rental.rent;
|
|
|
|
|
|
if (newRoomRent > 0) {
|
|
|
|
|
|
const billStartDate = changeDate || new Date();
|
|
|
|
|
|
const billMonth = rentalBillingService.billMonthOf(billStartDate);
|
|
|
|
|
|
await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: newRental.id,
|
|
|
|
|
|
roomId: newRoomId,
|
|
|
|
|
|
renterId: rental.renterId,
|
|
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'rent',
|
|
|
|
|
|
receivableAmount: newRoomRent,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
|
|
|
|
|
billDate: billStartDate,
|
|
|
|
|
|
billMonth: billMonth,
|
|
|
|
|
|
...buildBillingMeta('change_room', rental.id),
|
|
|
|
|
|
remark: `换房首期租金 - ${renterName}`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 更新新房间状态为已租
|
|
|
|
|
|
await Room.update(
|
|
|
|
|
|
{ status: 'rented', rentalStatus: 'normal' },
|
|
|
|
|
|
{ where: { id: newRoomId }, transaction }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await transaction.commit();
|
|
|
|
|
|
|
|
|
|
|
|
response.success(res, '换房办理成功', {
|
|
|
|
|
|
oldRentalId: rental.id,
|
|
|
|
|
|
newRentalId: newRental.id,
|
|
|
|
|
|
oldRoomId: oldRoomId,
|
|
|
|
|
|
newRoomId: newRoomId,
|
|
|
|
|
|
changeDate: changeDate
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
console.error('换房处理时出错:', error);
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 创建租房(包含租客、水电表读数)- 整合接口
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 注意:入住时只记录水电表起始读数,不生成账单,水电费通过后续抄表生成
|
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,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 水电表读数(入住时记录,不生成账单)
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 检查房间状态
|
|
|
|
|
|
const room = await Room.findOne({
|
|
|
|
|
|
where: { id: roomId, tenantId, isDeleted: 0 },
|
|
|
|
|
|
transaction
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!room) {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
return response.notFound(res, '房间不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (room.status === 'rented') {
|
|
|
|
|
|
await transaction.rollback();
|
|
|
|
|
|
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}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 4. 创建首期租金账单(根据付款周期生成相应期数)
|
|
|
|
|
|
// 简化处理:默认只生成首期账单,后续通过定时任务或手动生成
|
|
|
|
|
|
const firstBillDate = startDate;
|
|
|
|
|
|
const firstBillMonth = startDate.substring(0, 7);
|
|
|
|
|
|
|
2026-04-20 06:43:09 +00:00
|
|
|
|
await Bill.create({
|
|
|
|
|
|
billNo: generateBillNo(),
|
|
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
roomId: parsedRoomId,
|
|
|
|
|
|
renterId: renter.id,
|
|
|
|
|
|
type: 'income',
|
|
|
|
|
|
category: 'rent',
|
|
|
|
|
|
receivableAmount: rent,
|
|
|
|
|
|
receivedAmount: 0,
|
|
|
|
|
|
status: 'unpaid',
|
2026-06-11 13:45:33 +00:00
|
|
|
|
billDate: firstBillDate,
|
|
|
|
|
|
billMonth: firstBillMonth,
|
|
|
|
|
|
settlementType: 'normal',
|
|
|
|
|
|
remark: `租约首期租金 - ${renterName}`,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
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',
|
2026-06-11 13:45:33 +00:00
|
|
|
|
billDate: firstBillDate,
|
|
|
|
|
|
billMonth: firstBillMonth,
|
|
|
|
|
|
settlementType: 'normal',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
remark: `租约押金 - ${renterName}`,
|
|
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 6. 记录水电表起始读数(仅记录,不生成账单)
|
|
|
|
|
|
// 水电费通过后续抄表管理生成账单
|
|
|
|
|
|
|
|
|
|
|
|
// 创建水表起始读数记录
|
2026-04-20 06:43:09 +00:00
|
|
|
|
if (waterStartReading !== null && waterStartReading !== '' && waterStartReading !== undefined) {
|
|
|
|
|
|
await MeterReading.create({
|
|
|
|
|
|
roomId: parsedRoomId,
|
|
|
|
|
|
renterId: renter.id,
|
|
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
meterType: 'water',
|
|
|
|
|
|
previousReading: 0,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
currentReading: parseFloat(waterStartReading),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
usage: 0,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
unitPrice: 0,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
amount: 0,
|
|
|
|
|
|
billMonth: startDate.substring(0, 7),
|
|
|
|
|
|
readingDate: startDate,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
remark: '入住时水表读数(起始读数)',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
2026-06-11 13:45:33 +00:00
|
|
|
|
console.log('水表起始读数记录创建成功');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
// 创建电表起始读数记录
|
2026-04-20 06:43:09 +00:00
|
|
|
|
if (electricStartReading !== null && electricStartReading !== '' && electricStartReading !== undefined) {
|
|
|
|
|
|
await MeterReading.create({
|
|
|
|
|
|
roomId: parsedRoomId,
|
|
|
|
|
|
renterId: renter.id,
|
|
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
meterType: 'electricity',
|
|
|
|
|
|
previousReading: 0,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
currentReading: parseFloat(electricStartReading),
|
2026-04-20 06:43:09 +00:00
|
|
|
|
usage: 0,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
unitPrice: 0,
|
2026-04-20 06:43:09 +00:00
|
|
|
|
amount: 0,
|
|
|
|
|
|
billMonth: startDate.substring(0, 7),
|
|
|
|
|
|
readingDate: startDate,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
remark: '入住时电表读数(起始读数)',
|
2026-04-20 06:43:09 +00:00
|
|
|
|
tenantId,
|
|
|
|
|
|
createBy,
|
|
|
|
|
|
updateBy: createBy
|
|
|
|
|
|
}, { transaction });
|
2026-06-11 13:45:33 +00:00
|
|
|
|
console.log('电表起始读数记录创建成功');
|
2026-04-20 06:43:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 提交事务
|
|
|
|
|
|
await transaction.commit();
|
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
|
response.created(res, '入住办理成功(首期租金和押金账单已生成,水电表读数已记录)', {
|
2026-04-22 06:48:32 +00:00
|
|
|
|
rentalId: rental.id,
|
|
|
|
|
|
renterId: renter.id,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
roomId: parsedRoomId,
|
|
|
|
|
|
billsGenerated: {
|
|
|
|
|
|
rent: true,
|
|
|
|
|
|
deposit: deposit > 0,
|
|
|
|
|
|
meterReadings: !!(waterStartReading || electricStartReading)
|
|
|
|
|
|
}
|
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,
|
2026-06-11 13:45:33 +00:00
|
|
|
|
createRentalWithRenter,
|
|
|
|
|
|
renewRental,
|
|
|
|
|
|
changeRoom
|
2026-04-20 06:43:09 +00:00
|
|
|
|
};
|