450 lines
12 KiB
JavaScript
450 lines
12 KiB
JavaScript
const { Bill, MeterReading, Room, Renter, Rental, BillPayment } = require('../models');
|
|
const { Op } = require('sequelize');
|
|
const response = require('../utils/response');
|
|
|
|
// 生成账单编号
|
|
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) => {
|
|
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 response.notFound(res, '账单不存在');
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
response.success(res, '更新成功', formatBillData(bill));
|
|
} catch (error) {
|
|
console.error('更新账单失败:', error);
|
|
response.serverError(res, '更新账单失败', error);
|
|
}
|
|
};
|
|
|
|
// 删除账单(软删除)
|
|
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 response.notFound(res, '账单不存在');
|
|
}
|
|
|
|
await bill.update({
|
|
isDeleted: 1,
|
|
updateBy: req.user.id
|
|
});
|
|
|
|
response.success(res, '账单删除成功');
|
|
} catch (error) {
|
|
console.error('删除账单失败:', error);
|
|
response.serverError(res, '删除账单失败', error);
|
|
}
|
|
};
|
|
|
|
// 账单收款
|
|
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 response.notFound(res, '账单不存在');
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
response.success(res, '收款成功', formatBillData(bill));
|
|
} catch (error) {
|
|
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
|
|
};
|