一、背景
(一)业务场景
1、零售互联网都会涉及到门店,很多报表都需要计算门店租金合计指标
门店租金合计:指指定日期区间分摊的租金
2、门店一般对应一个门店id,但是可能存在多个门面房
(二)相关数据支撑
门店租金信息表格:一个门店id可能有多条数据(多个门面房)
包含基本信息:门店ID,初始租金(第一次签订的合同租金,一般指一年),合同开始时间,合同结束时间,租金递增方式、组件递增类型、租金递增系数
在这里插入代码片store_rent表建表语句如下
create table store_rent
(
id bigint not null primary key,
store_id bigint null comment '门店id',
company_id int null comment '公司id',
landlord_name varchar(255) null comment '房东姓名',
landlord_phone varchar(20) null comment '房东电话',
landlord_type varchar(255) null comment '房东类型【个人 / 企业】',
rent_money decimal(19, 2) null comment '租金',
rent_start_time datetime null comment '租赁开始时间',
rent_end_time datetime null comment '租赁结束时间',
rent_incremental_method varchar(255) null comment '租金递增方式【无递增/每年/第二年/第三年/第四年/第五年】',
rent_incremental_type varchar(255) null comment '租金递增类型【固定金额/固定比例】',
rent_incremental_coefficient decimal(19, 2) null comment '租金递增系数【百分数或者金额】',
property_fee decimal(19, 2) null comment '物业费',
deposit decimal(19, 2) null comment '押金',
deposit_apportion_year int null comment '押金分摊年限',
transfer_fee decimal(19, 2) null comment '转让费',
transfer_fee_apportion_year int null comment '转让费分摊年限',
bank_account varchar(255) null comment '银行账号',
bank_name varchar(255) null comment '开户行',
bank_account_name varchar(255) null comment '开户人'
)
row_format = DYNAMIC;
二、计算方法统一封装
(一)门店租金数据表格逻辑
1、门店租金信息的合同起始或者截止时间不能为空
2、租金递增方式如果是无递增,递增类型和递增系数可以为空;
其他递增方式,递增类型和递增系数不可以为空
3、递增方式
无递增:每年租金都是一样的
每年/第二年:意思相同,都是从第二年开始租金递增
第三年/第四年/第五年/:都是指第几年开始递增
4、递增类型
如果是固定金额,那么递增系数就是金额
如果是固定比例,那么递增系数就是百分数
(二)业务逻辑详细解释
1、年真实租金计算逻辑:
rent_money:初始租金金额(第一个合同周期的年租金)
rent_incremental_method:租金递增方式【无递增/每年/第二年/第三年/第四年/第五年】
rent_incremental_type :租金递增类型【固定金额/固定比例】
rent_incremental_coefficient:租金递增系数【百分数或者金额】
根据租赁增长周期,增长方式和系数计算出对应合同周期的租金
2、日租金rent_fee=对应合同周期的年真实租金/周期总天数)
3、示例1:门店id为1001的日期区间为String[] bizday=[20231129,20231201],根据下面条件计算下总租金
初始租金rent_money是10000
合同开始日期rent_start_time:20221130
合同截止日期rent_end_time:20251130
租金递增方式rent_incremental_method:每年
递增模式rent_incremental_type:按固定金额
递增系数rent_incremental_coefficient:10000
解答:
3.1根据已知条件[20231129,20231201]和[20221131,20251130]有交集
3.2 合同的每个周期就是一年左右,合同第一个周期是20221130-20231129,第二个周期是20231130-20241129,第三个周期是20241130-20251129
3.3那么bizday属于1,2两个周期,第一个周期占了了1天,第二周期占了2天
3.4第一个周期的年真实租金是10000,周期总天数是365,所以第一个周期的日租金是10000/365
第二个周期的年真实租金是10000+10000=20000,周期总天数是365,所以第二个周期的日租金是20000/365
3.5bizday在第一个周期占用了1天,在第二个周期占用了2天,所以第一个周期的总租金是(10000/365)1,第二个周期的总租金是(20000/365)2
最后得出bizday日期区间的总租金是1(10000/365)+2(20000/365)
4、示例2:门店id为1001的日期区间为String[] bizday=[20241129,20241201],根据下面条件计算下总租金
初始租金rent_money是10000
合同开始日期rent_start_time:20221130
合同截止日期rent_end_time:20251129
租金递增方式rent_incremental_method:3年
递增模式rent_incremental_type:按固定金额
递增系数rent_incremental_coefficient:10000
解答:
4.1根据已知条件[20241129,20241201]和[20221130,20251129]有交集
4.2 合同的每个周期就是一年左右,合同第一个周期是20221130-20231129,第二个周期是20231130-20241129,第三个周期是20241130-20251129
4.3那么bizday属于1,2两个周期,第一个周期占了了1天,第二周期占了2天
4.4第一个周期的年真实租金是10000,周期总天数是365,所以第一个周期的日租金是10000/365
第二个周期的年真实租金是10000,周期总天数是365,所以第二个周期的日租金是10000/365
第三个周期的年真实租金是10000+10000=20000,周期总天数是365,所以第三个周期的日租金是20000/365
4.5bizday在第二个周期占用了1天,在第三个周期占用了2天,所以第二个周期的总租金是(10000/365)1,第二个周期的总租金是(20000/365)2
最后得出bizday日期区间的总租金是1(10000/365)+2(20000/365)
5、如果是递增类型是系数,同理。
(三)具体代码
public class RentCalculatorUtil {
/**
* 计算指定查询区间内各门店租金总和
* @param queryStart 查询开始日期
* @param queryEnd 查询结束日期
* @param storeRents 租金合同列表(合同起止时间均不为空)
* @return key为门店id,value为总租金
*/
public static Map<Long, BigDecimal> calculateTotalRent(Date queryStart, Date queryEnd, List<StoreRentDetailDTO> storeRents) {
if (CollectionUtil.isEmpty(storeRents)){
return Map.of();
}
Map<Long, BigDecimal> rentMap = Map.of();;
for (StoreRentDetailDTO rent : storeRents) {
Long storeId = rent.getStoreId();
Date contractStart = rent.getRentStartTime();
Date contractEnd = rent.getRentEndTime();
// 求合同与查询区间的交集
Date effectiveQueryStart = maxDate(contractStart, queryStart);
Date effectiveQueryEnd = minDate(contractEnd, queryEnd);
if (effectiveQueryStart.after(effectiveQueryEnd)) {
continue;
}
BigDecimal totalRent = BigDecimal.ZERO;
// 按“每年一个周期”划分,周期定义为:[周期起始, 起始日期+1年-1天]
Calendar cycleStartCal = Calendar.getInstance();
cycleStartCal.setTime(contractStart);
int cycleIndex = 0; // cycleIndex==0 表示第一周期
while (true) {
Date cycleStart = cycleStartCal.getTime();
Calendar cycleEndCal = (Calendar) cycleStartCal.clone();
cycleEndCal.add(Calendar.YEAR, 1);
cycleEndCal.add(Calendar.DATE, -1);
Date cycleEnd = cycleEndCal.getTime();
// 若当前周期结束超出合同结束,则以合同结束日期为周期结束
if (cycleEnd.after(contractEnd)) {
cycleEnd = contractEnd;
}
// 求当前周期与查询区间的交集
Date cycleEffectiveStart = maxDate(cycleStart, queryStart);
Date cycleEffectiveEnd = minDate(cycleEnd, queryEnd);
if (!cycleEffectiveStart.after(cycleEffectiveEnd)) {
long effectiveDays = daysBetween(cycleEffectiveStart, cycleEffectiveEnd) + 1;
//long cycleTotalDays = daysBetween(cycleStart, cycleEnd) + 1;
// 计算当前周期的年租金,根据递增规则与递增类型决定
BigDecimal annualRent = computeAnnualRent(
rent.getRentMoney(),
rent.getRentIncrementalMethod(),
rent.getRentIncrementalType(),
rent.getRentIncrementalCoefficient(),
cycleIndex);
BigDecimal dailyRent = annualRent.divide(BigDecimal.valueOf(365), 10, BigDecimal.ROUND_HALF_UP);
totalRent = totalRent.add(dailyRent.multiply(BigDecimal.valueOf(effectiveDays)));
}
// 准备下个周期:当前周期结束后一天为下个周期起始
Calendar nextCycleStartCal = Calendar.getInstance();
nextCycleStartCal.setTime(cycleEnd);
nextCycleStartCal.add(Calendar.DATE, 1);
if (nextCycleStartCal.getTime().after(contractEnd)) {
break;
}
cycleStartCal = nextCycleStartCal;
cycleIndex++;
}
BigDecimal previousRent = rentMap.getOrDefault(storeId, BigDecimal.ZERO);
rentMap.put(storeId, previousRent.add(totalRent));
}
return rentMap;
}
/**
* 根据递增规则计算当前周期的年租金
* 仅处理 FIXED_MONEY 和 FIXED_RATE 两种递增类型,规则如下:
*
* 1. 对于 EVERY_YEAR 和 TWO_YEAR(效果相同):从第2周期开始递增
* - 如果递增类型是固定金额:
* 年租金 = baseRent, 当 cycleIndex == 0;
* 当 cycleIndex >= 1,年租金 = baseRent + (cycleIndex) * coefficient
* - 如果递增类型是固定比例:
* 年租金 = baseRent, 当 cycleIndex == 0;
* 当 cycleIndex >= 1,年租金 = baseRent * (1 + coefficient)
*
* 2. 对于 THREE_YEAR:从第3周期开始递增
* - FIXED_MONEY:cycleIndex < 2时年租金 = baseRent;cycleIndex>=2时,年租金 = baseRent + (cycleIndex - 1)*coefficient
* - FIXED_RATE:cycleIndex < 2时年租金 = baseRent;cycleIndex>=2时,年租金 = baseRent * (1 + coefficient)
*
* 3. FOUR_YEAR:从第4周期开始递增
* - FIXED_MONEY:当 cycleIndex < 3 时,年租金 = baseRent;否则 = baseRent + (cycleIndex - 2)*coefficient
* - FIXED_RATE:当 cycleIndex < 3 时,年租金 = baseRent;否则 = baseRent * (1 + coefficient)
*
* 4. FIVE_YEAR:从第5周期开始递增
* - FIXED_MONEY:当 cycleIndex < 4 时,年租金 = baseRent;否则 = baseRent + (cycleIndex - 3)*coefficient
* - FIXED_RATE:当 cycleIndex < 4 时,年租金 = baseRent;否则 = baseRent * (1 + coefficient)
*
* @param baseRent 合同初始租金
* @param method 递增方式
* @param type 递增类型(FIXED_MONEY 或 FIXED_RATE)
* @param coefficient 递增系数(FIXED_MONEY时为金额,FIXED_RATE时为乘数,例如2表示 1+2=3倍)
* @param cycleIndex 当前周期编号(0表示第一周期)
* @return 当前周期的年租金
*/
private static BigDecimal computeAnnualRent(BigDecimal baseRent,
StoreRentDetailDTO.RentIncrementalMethodEnum method,
StoreRentDetailDTO.RentIncrementalTypeEnum type,
BigDecimal coefficient,
int cycleIndex) {
// 根据递增方式确定起始递增周期(threshold)
int threshold;
switch (method) {
case EVERY_YEAR:
case TWO_YEAR:
threshold = 1;
break;
case THREE_YEAR:
threshold = 2;
break;
case FOUR_YEAR:
threshold = 3;
break;
case FIVE_YEAR:
threshold = 4;
break;
default:
threshold = 1;
}
// 若当前周期在递增阈值之前,则不递增
if (cycleIndex < threshold) {
return baseRent;
}
// 递增模式如果是NONE,说明每个周期都是相同的租金
if (method == StoreRentDetailDTO.RentIncrementalMethodEnum.NONE) {
return baseRent;
}
// 针对两种递增类型分别处理:
if (type == StoreRentDetailDTO.RentIncrementalTypeEnum.FIXED_MONEY) {
// 递增次数 = cycleIndex - threshold + 1
int increaseCount = cycleIndex - threshold + 1;
return baseRent.add(coefficient.multiply(BigDecimal.valueOf(increaseCount)));
} else if (type == StoreRentDetailDTO.RentIncrementalTypeEnum.FIXED_RATE) {
// 计算从递增开始到当前周期的总增长倍数
int periodsPassedThreshold = cycleIndex - threshold + 1;
BigDecimal totalMultiplier = BigDecimal.ONE.add(coefficient).pow(periodsPassedThreshold);
return baseRent.multiply(totalMultiplier);
}
return baseRent;
}
private static Date maxDate(Date d1, Date d2) {
return d1.after(d2) ? d1 : d2;
}
private static Date minDate(Date d1, Date d2) {
return d1.before(d2) ? d1 : d2;
}
/**
* 返回两个日期之间的整天数(不含首日,因此计算时需加1表示包含首尾)
*/
private static long daysBetween(Date d1, Date d2) {
long diffMs = d2.getTime() - d1.getTime();
return diffMs / (1000 * 60 * 60 * 24);
}
/**
* 测试方法
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
List<StoreRentDetailDTO> storeRents = new ArrayList<>();
// 示例查询区间
Date startTime = java.sql.Date.valueOf("2024-11-29");
Date endTime = java.sql.Date.valueOf("2024-12-01");
StoreRentDetailDTO storeRent = new StoreRentDetailDTO();
storeRent.setStoreId(100002L);
storeRent.setRentStartTime(java.sql.Date.valueOf("2022-11-30"));
storeRent.setRentEndTime(java.sql.Date.valueOf("2028-12-30"));
storeRent.setRentMoney(new BigDecimal("10000"));
// 修改此处测试不同递增方式和类型
// 以下示例测试 THREE_YEAR 方式:
// 1. 当递增类型为 FIXED_MONEY,coefficient 为 10000 时:
// 第1周期(2022-11-30至2023-11-29):年租金 = 10000
// 第2周期(2023-11-30至2024-11-29):年租金 = 10000(不递增)
// 第3周期(2024-11-30至2025-11-29):年租金 = 10000 + 1*10000 = 20000
// 2. 当递增类型为 FIXED_RATE,coefficient 为 2 时:
// 第1、2周期:年租金 = 10000
// 第3周期:年租金 = 10000*(1+2) = 30000
// 注意:后续周期仍保持 30000
storeRent.setRentIncrementalMethod(StoreRentDetailDTO.RentIncrementalMethodEnum.THREE_YEAR);
// 这里可切换递增类型:FIXED_MONEY 或 FIXED_RATE
// 示例:FIXED_MONEY
// storeRent.setRentIncrementalType(StoreRent.RentIncrementalTypeEnum.FIXED_MONEY);
// storeRent.setRentIncrementalCoefficient(new BigDecimal("10000"));
// 示例:FIXED_RATE
// storeRent.setRentIncrementalType(StoreRent.RentIncrementalTypeEnum.FIXED_RATE);
// storeRent.setRentIncrementalCoefficient(new BigDecimal("2")); // 表示1+2倍
storeRent.setRentIncrementalType(StoreRentDetailDTO.RentIncrementalTypeEnum.FIXED_MONEY);
storeRent.setRentIncrementalCoefficient(new BigDecimal("10000")); // 表示1+2倍
storeRents.add(storeRent);
Map<Long, BigDecimal> totalRents = calculateTotalRent(startTime, endTime, storeRents);
System.out.println(totalRents);
// 根据示例:
// 若递增方式为 THREE_YEAR 且递增类型为 FIXED_MONEY,则:
// - 第1周期(2022-11-30至2023-11-29):年租金 = 10000(不递增)
// - 第2周期(2023-11-30至2024-11-29):年租金 = 10000(不递增),交集仅一天(2024-11-29)
// - 第3周期(2024-11-30至2025-11-29):年租金 = 10000+10000=20000,交集2天(2024-11-30、2024-12-01)
// 总租金 = 1*(10000/365) + 2*(20000/365) ≈ 136.91
//
// 如果递增类型为 FIXED_RATE,则:
// - 第1、2周期年租金 = 10000;第3周期 = 10000*(1+2)=30000
// 总租金 = 1*(10000/365) + 2*(30000/365)≈ 191.78
}
}