const { Rental, Room, Apartment, Bill, Transaction, Renter, MeterReading } = require('../models'); const { Op } = require('sequelize'); const response = require('../utils/response'); const rentalBillingService = require('../services/rentalBillingService'); // 格式化时间(考虑时区,转换为北京时间) 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]; }; // 格式化日期时间(年月日时分秒) 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); }; // 格式化租房数据 const formatRentalData = (rental) => { const formattedRental = { ...rental.toJSON(), startDate: formatDate(rental.startDate), endDate: formatDate(rental.endDate), createTime: formatDateTime(rental.createTime), updateTime: formatDateTime(rental.updateTime) }; return formattedRental; }; 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 }); // 检查并更新租房状态 const checkAndUpdateRentalStatus = async (tenantId) => { try { // 获取当前日期 const currentDate = new Date(); // 计算5天后的日期 const fiveDaysLater = new Date(); fiveDaysLater.setDate(currentDate.getDate() + 5); // 查找该租户下所有活跃的租房记录 const rentals = await Rental.findAll({ where: { tenantId, 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(req.user.tenantId); const { apartmentId, roomId, renterName, status, startDateFrom, startDateTo, endDateFrom, endDateTo, page = 1, pageSize = 10 } = req.query; // 构建查询条件 const where = { tenantId: req.user.tenantId, 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, tenantId: req.user.tenantId, isDeleted: 0 }, include: [ { model: Room, where: { isDeleted: 0 }, include: [ { model: Apartment } ] }, { 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'); } // 检查房间是否存在 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, '该房间已有进行中的租赁合同'); } // 处理押金,为空时设置为0 const deposit = body.deposit || 0; // 获取租客信息用于账单备注 const renter = await Renter.findByPk(parsedRenterId); const renterName = renter ? renter.name : ''; var transaction; // 创建租房记录 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 }); transaction = await require('../config/db').transaction(); 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, tenantId: req.user.tenantId, createBy: req.user.id, updateBy: req.user.id }, { transaction }); console.log('租房记录:', rental); // 更新房间状态为已租 await Room.update({ status: 'rented', rentalStatus: 'normal' }, { where: { id: parsedRoomId }, transaction }); // 自动生成租金账单 await Bill.create({ billNo: 'B' + Date.now(), rentalId: rental.id, roomId: parsedRoomId, renterId: parsedRenterId, type: 'income', category: 'rent', receivableAmount: body.rent, receivedAmount: 0, status: 'unpaid', billDate: body.startDate, billMonth: body.startDate.substring(0, 7), settlementType: 'normal', remark: `租约租金 - ${renterName}`, tenantId: req.user.tenantId, createBy: req.user.id, updateBy: req.user.id }, { transaction }); // 自动生成押金账单(如果有押金) if (deposit > 0) { await Bill.create({ billNo: 'B' + Date.now() + '1', rentalId: rental.id, roomId: parsedRoomId, renterId: parsedRenterId, type: 'income', category: 'deposit', receivableAmount: deposit, receivedAmount: 0, status: 'unpaid', billDate: body.startDate, billMonth: body.startDate.substring(0, 7), settlementType: 'normal', remark: `租约押金 - ${renterName}`, tenantId: req.user.tenantId, createBy: req.user.id, updateBy: req.user.id }, { transaction }); } await transaction.commit(); response.created(res, '创建成功', rental); } catch (error) { if (typeof transaction !== 'undefined' && transaction) { await transaction.rollback(); } 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, tenantId: req.user.tenantId, isDeleted: 0 }, include: [{ model: Room }] }); if (!rental) { return response.notFound(res, '租房记录不存在'); } // 状态校验:已终止/已到期的租约不能编辑 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 } ); } // 处理押金,为空时设置为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 }, { transaction }); const hasPaidBills = await Bill.count({ where: { rentalId: id, tenantId: req.user.tenantId, status: { [Op.in]: ['paid', 'partial'] }, isDeleted: 0 }, transaction }); 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(); response.success(res, '更新成功', rental); } catch (error) { if (typeof transaction !== 'undefined' && transaction) { await transaction.rollback(); } response.serverError(res, '更新租约失败', error); } }; // 删除租约(软删除) const deleteRental = async (req, res) => { const transaction = await require('../config/db').transaction(); try { const { id } = req.params; const rental = await Rental.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 }, transaction }); if (!rental) { await transaction.rollback(); return response.notFound(res, '租约不存在'); } await rentalBillingService.assertRentalCanBeDeleted({ rentalId: id, tenantId: req.user.tenantId, transaction }); await rentalBillingService.cancelUnpaidBillsForRental({ rentalId: id, tenantId: req.user.tenantId, operatorId: req.user.id, transaction }); await rental.update({ isDeleted: 1, updateBy: req.user.id }, { transaction }); const otherActiveRental = await Rental.findOne({ where: { id: { [Op.ne]: id }, roomId: rental.roomId, tenantId: req.user.tenantId, status: 'active', isDeleted: 0 }, transaction }); if (!otherActiveRental) { await Room.update( { status: 'empty', rentalStatus: 'normal' }, { where: { id: rental.roomId, tenantId: req.user.tenantId }, transaction } ); } await transaction.commit(); response.success(res, '租约删除成功'); } catch (error) { await transaction.rollback(); if (error.statusCode === 400) { return response.badRequest(res, error.message); } response.serverError(res, '删除租房失败', error); } }; // 退租处理(优化版:结算水电费 + 退还押金) const terminateRental = async (req, res) => { const transaction = await require('../config/db').transaction(); try { const { id } = req.params; const { waterMeterEnd, electricityMeterEnd, refundDeposit, remark } = req.body; const rental = await Rental.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 }, include: [ { model: Room, where: { isDeleted: 0 }, include: [{ model: Apartment, 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: { [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} 笔未结清的账单,请先结清后再退租`); } const room = rental.Room; const apartment = room.Apartment; const renterName = rental.Renter ? rental.Renter.name : ''; const createBy = req.user.id; const tenantId = req.user.tenantId; const billsGenerated = []; // 1. 更新租约信息 await rental.update({ status: 'terminated', closeReason: 'terminated', waterMeterEnd: waterMeterEnd || null, electricityMeterEnd: electricityMeterEnd || null, remark: remark ? `${rental.remark || ''}\n退租备注:${remark}` : rental.remark, updateBy: createBy }, { transaction }); // 2. 生成水电费账单(如有读数差异) if (waterMeterEnd && rental.waterMeterStart) { const waterUsage = parseFloat(waterMeterEnd) - parseFloat(rental.waterMeterStart); if (waterUsage > 0) { const waterAmount = waterUsage * parseFloat(apartment.waterPrice || 0); const bill = await Bill.create({ billNo: generateBillNo(), rentalId: id, roomId: room.id, renterId: rental.renterId, type: 'income', category: 'water', receivableAmount: waterAmount, receivedAmount: 0, status: 'unpaid', billDate: new Date(), settlementType: 'checkout', sourceEventId: id, remark: `退租水费 - ${renterName}(用量:${waterUsage.toFixed(2)}吨)`, tenantId, createBy, updateBy: createBy }, { transaction }); billsGenerated.push({ category: 'water', amount: waterAmount }); } } if (electricityMeterEnd && rental.electricityMeterStart) { const electricityUsage = parseFloat(electricityMeterEnd) - parseFloat(rental.electricityMeterStart); if (electricityUsage > 0) { const electricityAmount = electricityUsage * parseFloat(apartment.electricityPrice || 0); const bill = await Bill.create({ billNo: generateBillNo(), rentalId: id, roomId: room.id, renterId: rental.renterId, type: 'income', category: 'electricity', receivableAmount: electricityAmount, receivedAmount: 0, status: 'unpaid', billDate: new Date(), settlementType: 'checkout', sourceEventId: id, remark: `退租电费 - ${renterName}(用量:${electricityUsage.toFixed(2)}度)`, tenantId, createBy, updateBy: createBy }, { transaction }); billsGenerated.push({ category: 'electricity', amount: electricityAmount }); } } // 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. 更新房间状态为空房 await Room.update( { status: 'empty', rentalStatus: 'normal' }, { where: { id: room.id }, transaction } ); 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 }); } catch (error) { await transaction.rollback(); console.error('退租处理时出错:', error); response.serverError(res, '退租处理失败', error); } }; // 续租接口 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); } }; // 获取所有租房(不分页) const listRentals = async (req, res) => { try { // 先检查并更新租房状态 await checkAndUpdateRentalStatus(req.user.tenantId); const { apartmentId, roomId, renterName, status, startDateFrom, startDateTo, endDateFrom, endDateTo } = req.query; // 构建查询条件 const where = { tenantId: req.user.tenantId, 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 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, '该房间已在租,请选择其他房间'); } 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. 创建首期租金账单(根据付款周期生成相应期数) // 简化处理:默认只生成首期账单,后续通过定时任务或手动生成 const firstBillDate = startDate; const firstBillMonth = startDate.substring(0, 7); await Bill.create({ billNo: generateBillNo(), rentalId: rental.id, roomId: parsedRoomId, renterId: renter.id, type: 'income', category: 'rent', receivableAmount: rent, receivedAmount: 0, status: 'unpaid', billDate: firstBillDate, billMonth: firstBillMonth, settlementType: 'normal', 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: firstBillDate, billMonth: firstBillMonth, settlementType: 'normal', remark: `租约押金 - ${renterName}`, tenantId, createBy, updateBy: createBy }, { transaction }); } // 6. 记录水电表起始读数(仅记录,不生成账单) // 水电费通过后续抄表管理生成账单 // 创建水表起始读数记录 if (waterStartReading !== null && waterStartReading !== '' && waterStartReading !== undefined) { await MeterReading.create({ roomId: parsedRoomId, renterId: renter.id, rentalId: rental.id, meterType: 'water', previousReading: 0, currentReading: parseFloat(waterStartReading), usage: 0, unitPrice: 0, amount: 0, billMonth: startDate.substring(0, 7), readingDate: startDate, remark: '入住时水表读数(起始读数)', tenantId, createBy, updateBy: createBy }, { transaction }); console.log('水表起始读数记录创建成功'); } // 创建电表起始读数记录 if (electricStartReading !== null && electricStartReading !== '' && electricStartReading !== undefined) { await MeterReading.create({ roomId: parsedRoomId, renterId: renter.id, rentalId: rental.id, meterType: 'electricity', previousReading: 0, currentReading: parseFloat(electricStartReading), usage: 0, unitPrice: 0, 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, billsGenerated: { rent: true, deposit: deposit > 0, meterReadings: !!(waterStartReading || electricStartReading) } }); } catch (error) { // 回滚事务 await transaction.rollback(); console.error('创建租房(整合)时出错:', error); response.serverError(res, '创建租房失败', error); } }; module.exports = { getAllRentals, listRentals, getRentalById, createRental, updateRental, deleteRental, terminateRental, createRentalWithRenter, renewRental, changeRoom };