331 lines
9.1 KiB
JavaScript
331 lines
9.1 KiB
JavaScript
|
|
/**
|
|||
|
|
* 计费服务 - 处理租户到期检查、超额计算等
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
};
|