rentease-backend-new/controllers/billController.js

475 lines
12 KiB
JavaScript
Raw Normal View History

2026-04-20 06:43:09 +00:00
const { Bill, MeterReading, Room, Renter, Rental, BillPayment } = require('../models');
const { Op } = require('sequelize');
// 生成账单编号
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);
// 返回结果
res.status(200).json({
code: 200,
data: formattedBills,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
} catch (error) {
console.error('获取账单列表失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
// 获取账单列表(不分页)
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);
// 返回结果
res.status(200).json({
code: 200,
data: formattedBills
});
} catch (error) {
console.error('获取账单列表失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
// 获取单个账单
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 res.status(404).json({ code: 404, error: '账单不存在' });
}
const formattedBill = formatBillData(bill);
res.status(200).json({
code: 200,
data: formattedBill,
message: 'success'
});
} catch (error) {
console.error('获取账单详情失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
// 创建账单
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);
res.status(201).json({
code: 201,
data: formattedBill,
message: '创建成功'
});
} catch (error) {
console.error('创建账单失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
// 更新账单
const updateBill = async (req, res) => {
try {
const { id } = req.params;
const {
roomId,
renterId,
rentalId,
type,
category,
receivableAmount,
billMonth,
billDate,
remark
} = req.body;
const bill = await Bill.findOne({
where: { id, tenantId: req.user.tenantId, isDeleted: 0 }
});
if (!bill) {
return res.status(404).json({ code: 404, error: '账单不存在' });
}
await bill.update({
roomId: roomId || null,
renterId: renterId || null,
rentalId: rentalId || null,
type,
category,
receivableAmount,
billMonth: billMonth || null,
billDate: billDate || bill.billDate,
remark,
updateBy: req.user.id
});
res.status(200).json({
code: 200,
data: formatBillData(bill),
message: '更新成功'
});
} catch (error) {
console.error('更新账单失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
// 删除账单(软删除)
const deleteBill = async (req, res) => {
try {
const { id } = req.params;
const bill = await Bill.findOne({
where: { id, tenantId: req.user.tenantId, isDeleted: 0 }
});
if (!bill) {
return res.status(404).json({ code: 404, error: '账单不存在' });
}
await bill.update({
isDeleted: 1,
updateBy: req.user.id
});
res.status(200).json({
code: 200,
message: '账单删除成功'
});
} catch (error) {
console.error('删除账单失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
// 账单收款
const receivePayment = async (req, res) => {
try {
const { id } = req.params;
const { amount, paymentMethod } = req.body;
const bill = await Bill.findOne({
where: { id, tenantId: req.user.tenantId, isDeleted: 0 }
});
if (!bill) {
return res.status(404).json({ code: 404, error: '账单不存在' });
}
const newReceivedAmount = parseFloat(bill.receivedAmount) + parseFloat(amount);
const receivableAmount = parseFloat(bill.receivableAmount);
let newStatus = 'unpaid';
if (newReceivedAmount >= receivableAmount) {
newStatus = 'paid';
} else if (newReceivedAmount > 0) {
newStatus = 'partial';
}
await bill.update({
receivedAmount: newReceivedAmount,
status: newStatus,
paymentMethod: paymentMethod || bill.paymentMethod,
updateBy: req.user.id
});
res.status(200).json({
code: 200,
data: formatBillData(bill),
message: '收款成功'
});
} catch (error) {
console.error('账单收款失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
// 获取账单统计
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 });
res.status(200).json({
code: 200,
data: {
summary: {
totalReceivable: totalReceivable || 0,
totalReceived: totalReceived || 0,
totalUnreceived: (totalReceivable || 0) - (totalReceived || 0)
},
income: incomeStats,
expense: expenseStats,
status: statusStats
}
});
} catch (error) {
console.error('获取账单统计失败:', error);
res.status(500).json({ code: 500, error: error.message });
}
};
module.exports = {
getAllBills,
getBillsList,
getBillById,
createBill,
updateBill,
deleteBill,
receivePayment,
getBillStatistics
};