rentease-backend-new/services/billingService.js

331 lines
9.1 KiB
JavaScript
Raw Permalink Normal View History

2026-04-20 06:43:09 +00:00
/**
* 计费服务 - 处理租户到期检查超额计算等
*/
const { Tenant, SubscriptionPlan, Order, PricingConfig } = require('../models');
const { Op } = require('sequelize');
const { logOperation } = require('../utils/logger');
/**
* 检查并更新租户到期状态
* 每天执行一次
*/
const checkTenantExpiration = async () => {
try {
const now = new Date();
console.log(`[${now.toISOString()}] 开始检查租户到期状态...`);
// 1. 检查试用期到期的租户
const trialExpiredTenants = await Tenant.findAll({
where: {
billingStatus: 'trial_active',
trialEndDate: {
[Op.lt]: now
}
}
});
for (const tenant of trialExpiredTenants) {
await tenant.update({ billingStatus: 'trial_expired' });
console.log(`租户 ${tenant.name} (ID: ${tenant.id}) 试用期已过期`);
// 记录日志
await logOperation({
tenantId: tenant.id,
module: '计费管理',
action: '试用期到期',
description: `租户 ${tenant.name} 试用期已过期`,
status: 'success'
});
}
// 2. 检查付费期到期的租户
const paidExpiredTenants = await Tenant.findAll({
where: {
billingStatus: 'paid_active',
paidEndDate: {
[Op.lt]: now
}
}
});
for (const tenant of paidExpiredTenants) {
await tenant.update({ billingStatus: 'paid_expired' });
console.log(`租户 ${tenant.name} (ID: ${tenant.id}) 付费期已过期`);
// 记录日志
await logOperation({
tenantId: tenant.id,
module: '计费管理',
action: '付费期到期',
description: `租户 ${tenant.name} 付费期已过期`,
status: 'success'
});
}
// 3. 检查数据保留期超过90天的租户可选发送提醒或清理
const retentionLimit = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
const longExpiredTenants = await Tenant.findAll({
where: {
billingStatus: {
[Op.in]: ['trial_expired', 'paid_expired']
},
updateTime: {
[Op.lt]: retentionLimit
}
}
});
if (longExpiredTenants.length > 0) {
console.log(`发现 ${longExpiredTenants.length} 个租户超过90天数据保留期`);
// 这里可以添加数据清理逻辑或发送提醒
}
console.log(`[${new Date().toISOString()}] 租户到期检查完成`);
console.log(`- 试用期过期: ${trialExpiredTenants.length}`);
console.log(`- 付费期过期: ${paidExpiredTenants.length}`);
return {
trialExpired: trialExpiredTenants.length,
paidExpired: paidExpiredTenants.length,
longExpired: longExpiredTenants.length
};
} catch (error) {
console.error('检查租户到期状态失败:', error);
throw error;
}
};
/**
* 获取即将到期的租户用于提醒
* @param {number} days - 提前多少天提醒
*/
const getUpcomingExpiredTenants = async (days = 7) => {
try {
const reminderDate = new Date();
reminderDate.setDate(reminderDate.getDate() + days);
const upcomingTenants = await Tenant.findAll({
where: {
[Op.or]: [
{
billingStatus: 'trial_active',
trialEndDate: {
[Op.lte]: reminderDate,
[Op.gte]: new Date()
}
},
{
billingStatus: 'paid_active',
paidEndDate: {
[Op.lte]: reminderDate,
[Op.gte]: new Date()
}
}
]
},
include: [{
model: SubscriptionPlan,
as: 'subscriptionPlan'
}]
});
return upcomingTenants;
} catch (error) {
console.error('获取即将到期租户失败:', error);
throw error;
}
};
/**
* 计算租户的超额使用量
* @param {number} tenantId - 租户ID
*/
const calculateOverage = async (tenantId) => {
try {
const tenant = await Tenant.findByPk(tenantId, {
include: [{
model: SubscriptionPlan,
as: 'subscriptionPlan'
}]
});
if (!tenant || !tenant.subscriptionPlan) {
throw new Error('租户或套餐不存在');
}
const plan = tenant.subscriptionPlan;
// 获取当前使用量
const { Apartment, Room, User } = require('../models');
const [apartmentCount, roomCount, userCount] = await Promise.all([
Apartment.count({ where: { tenantId, isDeleted: 0 } }),
Room.count({ where: { tenantId, isDeleted: 0 } }),
User.count({ where: { tenantId, isDeleted: 0 } })
]);
// 计算超额
const overageApartments = Math.max(0, apartmentCount - plan.maxApartments);
const overageRooms = Math.max(0, roomCount - plan.maxRooms);
const overageUsers = Math.max(0, userCount - plan.maxUsers);
return {
usage: {
apartments: apartmentCount,
rooms: roomCount,
users: userCount
},
limits: {
apartments: plan.maxApartments,
rooms: plan.maxRooms,
users: plan.maxUsers
},
overage: {
apartments: overageApartments,
rooms: overageRooms,
users: overageUsers
}
};
} catch (error) {
console.error('计算超额使用量失败:', error);
throw error;
}
};
/**
* 计算续费订单金额
* @param {number} tenantId - 租户ID
* @param {number} planId - 套餐ID
* @param {number} months - 购买月数
*/
const calculateRenewalAmount = async (tenantId, planId, months) => {
try {
// 获取套餐信息
const plan = await SubscriptionPlan.findByPk(planId);
if (!plan) {
throw new Error('套餐不存在');
}
// 获取当前价格配置
const pricingConfig = await PricingConfig.findOne({
where: { isActive: true },
order: [['effectiveDate', 'DESC']]
});
if (!pricingConfig) {
throw new Error('价格配置不存在');
}
// 计算超额使用费
const overage = await calculateOverage(tenantId);
// 基础费用 = 套餐月费 × 月数
const baseAmount = plan.monthlyPrice * months;
// 超额费用 = (超额公寓×单价 + 超额房间×单价 + 超额用户×单价) × 月数
const overageAmount = (
overage.overage.apartments * pricingConfig.overageApartmentPrice +
overage.overage.rooms * pricingConfig.overageRoomPrice +
overage.overage.users * pricingConfig.overageUserPrice
) * months;
const totalAmount = baseAmount + overageAmount;
return {
baseAmount: parseFloat(baseAmount.toFixed(2)),
overageAmount: parseFloat(overageAmount.toFixed(2)),
totalAmount: parseFloat(totalAmount.toFixed(2)),
details: {
planName: plan.name,
monthlyPrice: plan.monthlyPrice,
months,
overage
}
};
} catch (error) {
console.error('计算续费金额失败:', error);
throw error;
}
};
/**
* 处理支付成功后的租户状态更新
* @param {number} tenantId - 租户ID
* @param {number} months - 续费月数
* @param {number} planId - 套餐ID可选用于更新资源配置
*/
const processPaymentSuccess = async (tenantId, months, planId = null) => {
try {
const tenant = await Tenant.findByPk(tenantId);
if (!tenant) {
throw new Error('租户不存在');
}
const now = new Date();
let paidStartDate, paidEndDate, billingStatus;
// 如果当前是付费期,则延长
if (tenant.billingStatus === 'paid_active' && tenant.paidEndDate && tenant.paidEndDate > now) {
paidStartDate = tenant.paidStartDate;
paidEndDate = new Date(tenant.paidEndDate.getTime() + months * 30 * 24 * 60 * 60 * 1000);
billingStatus = 'paid_active';
} else {
// 新付费期或从过期状态恢复
paidStartDate = now;
paidEndDate = new Date(now.getTime() + months * 30 * 24 * 60 * 60 * 1000);
billingStatus = 'paid_active';
}
// 构建更新数据
const updateData = {
billingStatus,
paidStartDate,
paidEndDate,
currentPeriodStart: paidStartDate,
currentPeriodEnd: paidEndDate
};
// 如果有套餐ID更新租户套餐和资源配置
if (planId) {
const plan = await SubscriptionPlan.findByPk(planId);
if (plan) {
updateData.planId = planId;
updateData.maxApartments = plan.maxApartments;
updateData.maxRooms = plan.maxRooms;
updateData.maxUsers = plan.maxUsers;
}
}
await tenant.update(updateData);
console.log(`租户 ${tenant.name} (ID: ${tenantId}) 续费成功,有效期至 ${paidEndDate.toISOString()}`);
// 记录日志
await logOperation({
tenantId: tenant.id,
module: '计费管理',
action: '续费成功',
description: `租户续费 ${months} 个月,有效期至 ${paidEndDate.toLocaleDateString()}`,
status: 'success'
});
return {
billingStatus,
paidStartDate,
paidEndDate
};
} catch (error) {
console.error('处理支付成功失败:', error);
throw error;
}
};
module.exports = {
checkTenantExpiration,
getUpcomingExpiredTenants,
calculateOverage,
calculateRenewalAmount,
processPaymentSuccess
};