const { Bill, MeterReading, Room, Renter, Rental, BillPayment } = require('../models'); const { Op } = require('sequelize'); const response = require('../utils/response'); const rentalBillingService = require('../services/rentalBillingService'); // 生成账单编号 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(1000 + Math.random() * 9000); return `B${year}${month}${day}${random}`; }; // 格式化时间(考虑时区,转换为北京时间) const formatDate = (date) => { if (!date) return null; // 确保 date 是 Date 对象 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; // 确保 date 是 Date 对象 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 formatBillData = (bill) => { const formattedBill = { ...bill.toJSON(), createTime: formatDateTime(bill.createTime), updateTime: formatDateTime(bill.updateTime), billDate: formatDate(bill.billDate) }; // 格式化关联数据 if (formattedBill.Room) { formattedBill.roomNumber = formattedBill.Room.roomNumber; formattedBill.apartmentName = formattedBill.Room.Apartment?.name; } if (formattedBill.Renter) { formattedBill.renterName = formattedBill.Renter.name; } if (formattedBill.Rental) { formattedBill.rentalInfo = formattedBill.Rental; } if (formattedBill.MeterReading) { formattedBill.meterReading = formattedBill.MeterReading; } return formattedBill; }; // 构建账单查询条件 const buildBillWhere = (req) => { const { type, category, status, roomId, renterId, rentalId, billMonth, startDate, endDate } = req.query; const where = { tenantId: req.user.tenantId, isDeleted: 0 }; if (type) { where.type = type; } if (category) { where.category = category; } if (status) { where.status = status; } if (roomId) { where.roomId = roomId; } if (renterId) { where.renterId = renterId; } if (rentalId) { where.rentalId = rentalId; } if (billMonth) { where.billMonth = billMonth; } if (startDate && endDate) { where.billDate = { [Op.between]: [startDate, endDate] }; } return where; }; // 账单关联查询配置 const billIncludeOptions = [ { model: Room, attributes: ['roomNumber'], include: [{ model: require('../models').Apartment, attributes: ['name'] }] }, { model: Renter, attributes: ['name', 'phone'] }, { model: Rental, attributes: ['startDate', 'endDate'] }, { model: MeterReading, as: 'meterReading', required: false, where: { isDeleted: 0 } } ]; // 获取所有账单(支持搜索和分页) const getAllBills = async (req, res) => { try { const { page = 1, pageSize = 10 } = req.query; const where = buildBillWhere(req); // 计算偏移量 const offset = (page - 1) * pageSize; // 查询账单数据 const { count, rows } = await Bill.findAndCountAll({ where, include: billIncludeOptions, limit: parseInt(pageSize), offset: parseInt(offset), order: [['createTime', 'DESC']] }); // 格式化数据 const formattedBills = rows.map(formatBillData); // 返回结果 response.success(res, '获取成功', { list: formattedBills, total: count, page: parseInt(page), pageSize: parseInt(pageSize) }); } catch (error) { console.error('获取账单列表失败:', error); response.serverError(res, '获取账单列表失败', error); } }; // 获取账单列表(不分页) const getBillsList = async (req, res) => { try { const where = buildBillWhere(req); // 查询所有账单数据(不分页) const rows = await Bill.findAll({ where, include: billIncludeOptions, order: [['createTime', 'DESC']] }); // 格式化数据 const formattedBills = rows.map(formatBillData); // 返回结果 response.success(res, '获取成功', formattedBills); } catch (error) { console.error('获取账单列表失败:', error); response.serverError(res, '获取账单列表失败', error); } }; // 获取单个账单 const getBillById = async (req, res) => { try { const { id } = req.params; const bill = await Bill.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 }, include: [ { model: Room, attributes: ['roomNumber'], include: [{ model: require('../models').Apartment, attributes: ['name'] }] }, { model: Renter, attributes: ['name', 'phone'] }, { model: Rental, attributes: ['startDate', 'endDate'] }, { model: MeterReading, as: 'meterReading', required: false, where: { isDeleted: 0 } }, { model: BillPayment, as: 'billPayments', required: false, where: { isDeleted: 0 }, order: [['paymentTime', 'DESC']] } ] }); if (!bill) { return response.notFound(res, '账单不存在'); } const formattedBill = formatBillData(bill); response.success(res, '获取成功', formattedBill); } catch (error) { console.error('获取账单详情失败:', error); response.serverError(res, '获取账单详情失败', error); } }; // 创建账单 const createBill = async (req, res) => { try { const { roomId, renterId, rentalId, type, category, receivableAmount, billMonth, billDate, remark } = req.body; const bill = await Bill.create({ billNo: generateBillNo(), roomId: roomId || null, renterId: renterId || null, rentalId: rentalId || null, type, category, receivableAmount, receivedAmount: 0, status: 'unpaid', billMonth: billMonth || null, billDate: billDate || new Date(), remark, tenantId: req.user.tenantId, createBy: req.user.id, updateBy: req.user.id }); const formattedBill = formatBillData(bill); response.created(res, '创建成功', formattedBill); } catch (error) { console.error('创建账单失败:', error); response.serverError(res, '创建账单失败', error); } }; // 更新账单(支持部分更新) const updateBill = async (req, res) => { const transaction = await require('../config/db').transaction(); try { const { id } = req.params; const { roomId, renterId, rentalId, type, category, receivableAmount, billMonth, billDate, remark, status } = req.body; const bill = await Bill.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 }, transaction }); if (!bill) { await transaction.rollback(); return response.notFound(res, '账单不存在'); } const hasReceived = ['paid', 'partial'].includes(bill.status) || parseFloat(bill.receivedAmount || 0) > 0; const financialFieldsChanged = [roomId, renterId, rentalId, type, category, receivableAmount, billMonth, billDate, status] .some(value => value !== undefined); if (hasReceived && financialFieldsChanged) { await transaction.rollback(); return response.badRequest(res, '该账单已有收款记录,不能修改金额、状态、类型、日期或关联对象'); } if (status === 'paid' && parseFloat(bill.receivedAmount || 0) < parseFloat(bill.receivableAmount || 0)) { await transaction.rollback(); return response.badRequest(res, '未收清的账单不能直接标记为已支付,请通过收款流水入账'); } const updates = { updateBy: req.user.id }; if (roomId !== undefined) updates.roomId = roomId; if (renterId !== undefined) updates.renterId = renterId; if (rentalId !== undefined) updates.rentalId = rentalId; if (type !== undefined) updates.type = type; if (category !== undefined) updates.category = category; if (receivableAmount !== undefined) updates.receivableAmount = receivableAmount; if (billMonth !== undefined) updates.billMonth = billMonth; if (billDate !== undefined) updates.billDate = billDate; if (remark !== undefined) updates.remark = remark; if (status !== undefined) updates.status = status; await bill.update(updates, { transaction }); await transaction.commit(); response.success(res, '更新成功', formatBillData(bill)); } catch (error) { await transaction.rollback(); console.error('更新账单失败:', error); response.serverError(res, '更新账单失败', error); } }; // 删除账单(软删除 + 级联删除支付记录) const deleteBill = async (req, res) => { const transaction = await require('../config/db').transaction(); try { const { id } = req.params; const bill = await Bill.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 }, transaction }); if (!bill) { await transaction.rollback(); return response.notFound(res, '账单不存在'); } const paymentCount = await BillPayment.count({ where: { billId: id, tenantId: req.user.tenantId, isDeleted: 0 }, transaction }); if (paymentCount > 0 || ['paid', 'partial'].includes(bill.status) || parseFloat(bill.receivedAmount || 0) > 0) { await transaction.rollback(); return response.badRequest(res, '该账单已有收款流水,不能删除。请保留流水,可通过冲销/退款方式处理。'); } await bill.update({ isDeleted: 1, status: 'cancelled', updateBy: req.user.id }, { transaction }); await transaction.commit(); response.success(res, '账单已作废并删除'); } catch (error) { await transaction.rollback(); console.error('删除账单失败:', error); response.serverError(res, '删除账单失败', error); } }; // 账单收款(支持幂等性) const receivePayment = async (req, res) => { const transaction = await require('../config/db').transaction(); try { const { id } = req.params; const { amount, paymentMethod, transactionNo, remark } = req.body; const bill = await Bill.findOne({ where: { id, tenantId: req.user.tenantId, isDeleted: 0 }, transaction }); if (!bill) { await transaction.rollback(); return response.notFound(res, '账单不存在'); } // 幂等性检查:如果提供了交易流水号,检查是否已存在相同的支付记录 if (transactionNo) { const existingPayment = await BillPayment.findOne({ where: { transactionNo, billId: id, isDeleted: 0 }, transaction }); if (existingPayment) { await transaction.rollback(); return response.badRequest(res, '该交易流水号已存在,请勿重复提交'); } } let paymentInfo; try { const remainingAmount = parseFloat(bill.receivableAmount) - parseFloat(bill.receivedAmount || 0); paymentInfo = rentalBillingService.validatePaymentAmount(bill, amount !== undefined ? amount : remainingAmount); } catch (validationError) { await transaction.rollback(); return response.badRequest(res, validationError.message); } await bill.update({ receivedAmount: paymentInfo.newReceivedAmount, status: paymentInfo.newStatus, paymentMethod: paymentMethod || bill.paymentMethod, updateBy: req.user.id }, { transaction }); // 创建支付记录 const billPayment = await BillPayment.create({ billId: id, amount: paymentInfo.paymentAmount, paymentMethod: paymentMethod || 'cash', paymentTime: new Date(), transactionNo: transactionNo || null, remark: remark || null, tenantId: req.user.tenantId, createBy: req.user.id }, { transaction }); await transaction.commit(); response.success(res, '收款成功', { bill: formatBillData(bill), payment: { id: billPayment.id, amount: billPayment.amount, paymentMethod: billPayment.paymentMethod, paymentTime: formatDateTime(billPayment.paymentTime) } }); } catch (error) { await transaction.rollback(); console.error('账单收款失败:', error); response.serverError(res, '账单收款失败', error); } }; // 获取账单统计 const getBillStatistics = async (req, res) => { try { const { startDate, endDate, billMonth } = req.query; const tenantId = req.user.tenantId; const where = { tenantId, isDeleted: 0 }; if (startDate && endDate) { where.billDate = { [Op.between]: [startDate, endDate] }; } if (billMonth) { where.billMonth = billMonth; } // 收入统计 const incomeStats = await Bill.findAll({ where: { ...where, type: 'income' }, attributes: [ 'category', [Bill.sequelize.fn('SUM', Bill.sequelize.col('receivableAmount')), 'totalReceivable'], [Bill.sequelize.fn('SUM', Bill.sequelize.col('receivedAmount')), 'totalReceived'] ], group: ['category'] }); // 支出统计 const expenseStats = await Bill.findAll({ where: { ...where, type: 'expense' }, attributes: [ 'category', [Bill.sequelize.fn('SUM', Bill.sequelize.col('receivableAmount')), 'totalReceivable'], [Bill.sequelize.fn('SUM', Bill.sequelize.col('receivedAmount')), 'totalReceived'] ], group: ['category'] }); // 状态统计 const statusStats = await Bill.findAll({ where, attributes: [ 'status', [Bill.sequelize.fn('COUNT', Bill.sequelize.col('id')), 'count'], [Bill.sequelize.fn('SUM', Bill.sequelize.col('receivableAmount')), 'totalReceivable'], [Bill.sequelize.fn('SUM', Bill.sequelize.col('receivedAmount')), 'totalReceived'] ], group: ['status'] }); // 计算汇总 const totalReceivable = await Bill.sum('receivableAmount', { where }); const totalReceived = await Bill.sum('receivedAmount', { where }); response.success(res, '获取成功', { summary: { totalReceivable: totalReceivable || 0, totalReceived: totalReceived || 0, totalUnreceived: (totalReceivable || 0) - (totalReceived || 0) }, income: incomeStats, expense: expenseStats, status: statusStats }); } catch (error) { console.error('获取账单统计失败:', error); response.serverError(res, '获取账单统计失败', error); } }; module.exports = { getAllBills, getBillsList, getBillById, createBill, updateBill, deleteBill, receivePayment, getBillStatistics };