门店租金指定日期区间计算

一、背景

(一)业务场景

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
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值