2026-06-11 13:45:33 +00:00
|
|
|
const { Tenant, SubscriptionPlan, TenantSubscription, Apartment, Room, User } = require('../models');
|
2026-04-20 06:43:09 +00:00
|
|
|
const { Op } = require('sequelize');
|
|
|
|
|
const { logOperation } = require('../utils/logger');
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const addCalendarMonths = (date, months) => {
|
|
|
|
|
const source = new Date(date);
|
|
|
|
|
const day = source.getDate();
|
|
|
|
|
const target = new Date(source);
|
|
|
|
|
target.setDate(1);
|
|
|
|
|
target.setMonth(target.getMonth() + Number(months));
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const lastDayOfTargetMonth = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
|
|
|
|
|
target.setDate(Math.min(day, lastDayOfTargetMonth));
|
|
|
|
|
return target;
|
|
|
|
|
};
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const getBillingCycle = (months) => {
|
|
|
|
|
if (Number(months) === 12) return 'yearly';
|
|
|
|
|
if (Number(months) === 1) return 'monthly';
|
|
|
|
|
return 'custom';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const normalizeAmount = (value) => Number.parseFloat(Number(value || 0).toFixed(2));
|
|
|
|
|
|
|
|
|
|
const checkTenantExpiration = async () => {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
|
|
|
|
const trialExpiredTenants = await Tenant.findAll({
|
|
|
|
|
where: {
|
|
|
|
|
billingStatus: 'trial_active',
|
|
|
|
|
trialEndDate: { [Op.lt]: now }
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
});
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
for (const tenant of trialExpiredTenants) {
|
|
|
|
|
await tenant.update({ billingStatus: 'trial_expired' });
|
|
|
|
|
await logOperation({
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
module: '计费管理',
|
|
|
|
|
action: '试用期到期',
|
|
|
|
|
description: `租户 ${tenant.code} 试用期已过期`,
|
|
|
|
|
status: 'success'
|
2026-04-20 06:43:09 +00:00
|
|
|
});
|
2026-06-11 13:45:33 +00:00
|
|
|
}
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const paidExpiredTenants = await Tenant.findAll({
|
|
|
|
|
where: {
|
|
|
|
|
billingStatus: 'paid_active',
|
|
|
|
|
paidEndDate: { [Op.lt]: now }
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const tenant of paidExpiredTenants) {
|
|
|
|
|
await tenant.update({ billingStatus: 'paid_expired' });
|
|
|
|
|
await TenantSubscription.update(
|
|
|
|
|
{ status: 'expired' },
|
|
|
|
|
{
|
|
|
|
|
where: {
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
status: 'active',
|
|
|
|
|
endDate: { [Op.lt]: now }
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
);
|
|
|
|
|
await logOperation({
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
module: '计费管理',
|
|
|
|
|
action: '付费期到期',
|
|
|
|
|
description: `租户 ${tenant.code} 付费期已过期`,
|
|
|
|
|
status: 'success'
|
2026-04-20 06:43:09 +00:00
|
|
|
});
|
|
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
trialExpired: trialExpiredTenants.length,
|
|
|
|
|
paidExpired: paidExpiredTenants.length,
|
|
|
|
|
longExpired: 0
|
|
|
|
|
};
|
2026-04-20 06:43:09 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getUpcomingExpiredTenants = async (days = 7) => {
|
2026-06-11 13:45:33 +00:00
|
|
|
const reminderDate = new Date();
|
|
|
|
|
reminderDate.setDate(reminderDate.getDate() + days);
|
|
|
|
|
|
|
|
|
|
return Tenant.findAll({
|
|
|
|
|
where: {
|
|
|
|
|
[Op.or]: [
|
|
|
|
|
{
|
|
|
|
|
billingStatus: 'trial_active',
|
|
|
|
|
trialEndDate: {
|
|
|
|
|
[Op.lte]: reminderDate,
|
|
|
|
|
[Op.gte]: new Date()
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
billingStatus: 'paid_active',
|
|
|
|
|
paidEndDate: {
|
|
|
|
|
[Op.lte]: reminderDate,
|
|
|
|
|
[Op.gte]: new Date()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
include: [{
|
|
|
|
|
model: SubscriptionPlan,
|
|
|
|
|
as: 'subscriptionPlan'
|
|
|
|
|
}]
|
|
|
|
|
});
|
2026-04-20 06:43:09 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calculateOverage = async (tenantId) => {
|
2026-06-11 13:45:33 +00:00
|
|
|
const tenant = await Tenant.findByPk(tenantId, {
|
|
|
|
|
include: [{
|
|
|
|
|
model: SubscriptionPlan,
|
|
|
|
|
as: 'subscriptionPlan'
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!tenant) {
|
|
|
|
|
throw new Error('租户不存在');
|
|
|
|
|
}
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
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 } })
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
usage: {
|
|
|
|
|
apartments: apartmentCount,
|
|
|
|
|
rooms: roomCount,
|
|
|
|
|
users: userCount
|
|
|
|
|
},
|
|
|
|
|
limits: {
|
|
|
|
|
apartments: tenant.maxApartments,
|
|
|
|
|
rooms: tenant.maxRooms,
|
|
|
|
|
users: tenant.maxUsers
|
|
|
|
|
},
|
|
|
|
|
overage: {
|
|
|
|
|
apartments: Math.max(0, apartmentCount - tenant.maxApartments),
|
|
|
|
|
rooms: Math.max(0, roomCount - tenant.maxRooms),
|
|
|
|
|
users: Math.max(0, userCount - tenant.maxUsers)
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
};
|
2026-04-20 06:43:09 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calculateRenewalAmount = async (tenantId, planId, months) => {
|
2026-06-11 13:45:33 +00:00
|
|
|
const plan = await SubscriptionPlan.findOne({
|
|
|
|
|
where: { id: planId, status: 'active', isDeleted: 0 }
|
|
|
|
|
});
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
if (!plan) {
|
|
|
|
|
throw new Error('套餐不存在或已停用');
|
|
|
|
|
}
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const normalizedMonths = Number.parseInt(months, 10);
|
|
|
|
|
if (!Number.isInteger(normalizedMonths) || normalizedMonths < 1 || normalizedMonths > 36) {
|
|
|
|
|
throw new Error('购买月数必须在1-36之间');
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const billingCycle = getBillingCycle(normalizedMonths);
|
|
|
|
|
const monthlyPrice = Number(plan.monthlyPrice || 0);
|
|
|
|
|
const yearlyPrice = Number(plan.yearlyPrice || 0);
|
|
|
|
|
const baseAmount = billingCycle === 'yearly' && yearlyPrice > 0
|
|
|
|
|
? yearlyPrice
|
|
|
|
|
: monthlyPrice * normalizedMonths;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
baseAmount: normalizeAmount(baseAmount),
|
|
|
|
|
overageAmount: 0,
|
|
|
|
|
discountAmount: 0,
|
|
|
|
|
totalAmount: normalizeAmount(baseAmount),
|
|
|
|
|
billingCycle,
|
|
|
|
|
details: {
|
|
|
|
|
planName: plan.name,
|
|
|
|
|
monthlyPrice: normalizeAmount(monthlyPrice),
|
|
|
|
|
yearlyPrice: normalizeAmount(yearlyPrice),
|
|
|
|
|
months: normalizedMonths,
|
|
|
|
|
billingCycle
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
};
|
|
|
|
|
};
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const processPaymentSuccess = async ({ order, payment, operatorId, transaction }) => {
|
|
|
|
|
const tenant = await Tenant.findByPk(order.tenantId, { transaction });
|
|
|
|
|
if (!tenant) {
|
|
|
|
|
throw new Error('租户不存在');
|
|
|
|
|
}
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const plan = await SubscriptionPlan.findByPk(order.planId, { transaction });
|
|
|
|
|
if (!plan) {
|
|
|
|
|
throw new Error('套餐不存在');
|
|
|
|
|
}
|
2026-04-20 06:43:09 +00:00
|
|
|
|
2026-06-11 13:45:33 +00:00
|
|
|
const now = new Date();
|
|
|
|
|
const currentEnd = tenant.paidEndDate ? new Date(tenant.paidEndDate) : null;
|
|
|
|
|
const startDate = tenant.billingStatus === 'paid_active' && currentEnd && currentEnd > now
|
|
|
|
|
? currentEnd
|
|
|
|
|
: now;
|
|
|
|
|
const endDate = addCalendarMonths(startDate, order.months);
|
|
|
|
|
const billingCycle = order.billingCycle || getBillingCycle(order.months);
|
|
|
|
|
|
|
|
|
|
await TenantSubscription.update(
|
|
|
|
|
{ status: 'expired' },
|
|
|
|
|
{
|
|
|
|
|
where: {
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
status: 'active',
|
|
|
|
|
isDeleted: 0
|
|
|
|
|
},
|
|
|
|
|
transaction
|
2026-04-20 06:43:09 +00:00
|
|
|
}
|
2026-06-11 13:45:33 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const subscription = await TenantSubscription.create({
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
planId: plan.id,
|
|
|
|
|
orderId: order.id,
|
|
|
|
|
status: 'active',
|
|
|
|
|
billingCycle,
|
|
|
|
|
months: order.months,
|
|
|
|
|
startDate,
|
|
|
|
|
endDate,
|
|
|
|
|
amount: order.actualAmount,
|
|
|
|
|
createBy: operatorId,
|
|
|
|
|
updateBy: operatorId
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
await tenant.update({
|
|
|
|
|
planId: plan.id,
|
|
|
|
|
billingStatus: 'paid_active',
|
|
|
|
|
paidStartDate: startDate,
|
|
|
|
|
paidEndDate: endDate,
|
|
|
|
|
currentPeriodStart: startDate,
|
|
|
|
|
currentPeriodEnd: endDate,
|
|
|
|
|
maxApartments: plan.maxApartments,
|
|
|
|
|
maxRooms: plan.maxRooms,
|
|
|
|
|
maxUsers: plan.maxUsers,
|
|
|
|
|
updateBy: operatorId
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
await order.update({
|
|
|
|
|
subscriptionId: subscription.id,
|
|
|
|
|
periodStart: startDate,
|
|
|
|
|
periodEnd: endDate
|
|
|
|
|
}, { transaction });
|
|
|
|
|
|
|
|
|
|
await logOperation({
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
userId: operatorId,
|
|
|
|
|
module: '计费管理',
|
|
|
|
|
action: '订阅生效',
|
|
|
|
|
description: `租户订阅 ${plan.name} ${order.months} 个月,有效期至 ${endDate.toLocaleDateString()}`,
|
|
|
|
|
status: 'success'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
subscription,
|
|
|
|
|
billingStatus: 'paid_active',
|
|
|
|
|
paidStartDate: startDate,
|
|
|
|
|
paidEndDate: endDate,
|
|
|
|
|
paymentId: payment ? payment.id : null
|
|
|
|
|
};
|
2026-04-20 06:43:09 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
checkTenantExpiration,
|
|
|
|
|
getUpcomingExpiredTenants,
|
|
|
|
|
calculateOverage,
|
|
|
|
|
calculateRenewalAmount,
|
|
|
|
|
processPaymentSuccess
|
|
|
|
|
};
|