# 年度经营统计分析增长率算法修改方案 ## 一、需求分析 ### 当前算法 - 增长率 = (本年总数 - 上年总数) / 上年总数 × 100% - 月均 = 总数 / 12 ### 新需求 1. **25年新开门店**:增长率 = 0 2. **增长率算法改为月均增长平均值**: - 增长率 = (本年月均 - 上年月均) / 上年月均 × 100% 3. **24年开业的门店特殊处理**: - 第一个月业绩不计入平均的基数 - 第一个月不计入月份数量 - 例如:大源2024.10月开业,实际业绩取数11月、12月,平均月份数 = 2 ## 二、修改逻辑梳理 ### 2.1 门店开业时间获取 - 从 `lq_mdxx` 表获取门店开业时间字段 `kysj` - 判断门店开业年份: - 如果 `kysj` 年份 = 2025,则为25年新开门店 - 如果 `kysj` 年份 = 2024,则为24年开业门店 ### 2.2 月均计算逻辑 #### 对于24年开业的门店(上年数据): 1. 获取开业月份:`openMonth = kysj.Month` 2. 计算有效月份数:`validMonths = 12 - openMonth`(排除开业当月) 3. 计算有效月份数据总和:只统计 `openMonth + 1` 到 `12` 月的数据 4. 月均 = 有效月份数据总和 / 有效月份数 #### 对于25年新开门店(本年数据): 1. 获取开业月份:`openMonth = kysj.Month` 2. 计算有效月份数:`validMonths = 12 - openMonth`(排除开业当月) 3. 计算有效月份数据总和:只统计 `openMonth + 1` 到 `12` 月的数据 4. 月均 = 有效月份数据总和 / 有效月份数 #### 对于其他门店: - 月均 = 总数 / 12 ### 2.3 增长率计算逻辑 ```csharp // 伪代码 if (开业年份 == 2025) { // 25年新开门店,增长率 = 0 growthRate = "0%"; } else { // 计算本年月均 decimal currentAvg = CalculateMonthlyAverage(本年数据, 开业时间, 当前年份); // 计算上年月均 decimal lastAvg = CalculateMonthlyAverage(上年数据, 开业时间, 上年年份); // 增长率 = (本年月均 - 上年月均) / 上年月均 × 100% if (lastAvg == 0) { growthRate = currentAvg > 0 ? "100%" : "0%"; } else { growthRate = ((currentAvg - lastAvg) / lastAvg * 100).ToString("F2") + "%"; } } ``` ### 2.4 月均计算方法 ```csharp private decimal CalculateMonthlyAverage( List data, DateTime? openTime, int year) { if (data == null || !data.Any()) return 0; // 如果没有开业时间,按12个月计算 if (openTime == null || openTime.Value.Year != year) { return data.Sum(x => fieldSelector(x)) / 12; } // 如果是开业当年,排除开业当月 int openMonth = openTime.Value.Month; int validMonthCount = 12 - openMonth; if (validMonthCount <= 0) return 0; // 只统计开业月之后的数据 var validData = data.Where(x => x.Month > openMonth).ToList(); decimal total = validData.Sum(x => fieldSelector(x)); return total / validMonthCount; } ``` ## 三、需要修改的接口 ### 3.1 月度统计相关接口(使用 `GetMonthlyStat` 方法) 以下接口都调用 `GetMonthlyStat` 方法,需要统一修改: 1. **`[HttpPost("GetTotalPerformanceStat")]`** - 业绩月度统计 2. **`[HttpPost("GetTotalConsumeStat")]`** - 消耗月度统计 3. **`[HttpPost("GetHeadCountStat")]`** - 人头月度统计 4. **`[HttpPost("GetProjectCountStat")]`** - 项目数月度统计 5. **`[HttpPost("GetPersonTimeStat")]`** - 人次月度统计 6. **`[HttpPost("GetMonthlyStat")]`** - 通用月度统计 **修改点**: - 修改 `GetMonthlyStat` 方法中的月均计算逻辑(第1017行、1020行) - 修改增长率计算逻辑(第1022行),改为基于月均计算 ### 3.2 事业部汇总统计接口 **`[HttpPost("GetBusinessUnitSummaryStat")]`** - 事业部汇总统计(宽表) **修改点**: - 第912-916行:所有增长率计算需要改为基于月均计算 - 需要为每个指标计算月均,然后基于月均计算增长率 ### 3.3 门店指标统计接口 **`[HttpPost("GetStoreIndicatorsStat")]`** - 门店指标统计 **修改点**: - 调用 `GetStoreIndicatorStat` 方法(第1116行) - 需要修改 `GetStoreIndicatorStat` 方法中的增长率计算逻辑 ### 3.4 事业部指标统计接口 **`[HttpPost("GetBuIndicatorsStat")]`** - 事业部指标统计 **修改点**: - 调用 `GetBuIndicatorStat` 方法(第1195行) - 需要修改 `GetBuIndicatorStat` 方法中的增长率计算逻辑 ### 3.5 核心方法修改 1. **`CalculateGrowthRate` 方法**(第1212行) - 需要重载或修改,支持基于月均的增长率计算 - 需要传入开业时间参数,判断是否为25年新开门店 2. **新增方法:`CalculateMonthlyAverage`** - 计算月均,考虑开业时间 - 排除开业当月的数据和月份数 3. **新增方法:`GetStoreOpenTime`** - 批量获取门店开业时间 - 返回 `Dictionary` 格式 ## 四、具体修改步骤 ### 步骤1:新增辅助方法 ```csharp /// /// 批量获取门店开业时间 /// private async Task> GetStoreOpenTimesAsync(List storeIds) { var stores = await _db.Queryable() .Where(x => storeIds.Contains(x.Id)) .Select(x => new { x.Id, x.Kysj }) .ToListAsync(); return stores.ToDictionary(x => x.Id, x => x.Kysj); } /// /// 计算月均(考虑开业时间) /// private decimal CalculateMonthlyAverage( List data, DateTime? openTime, int year, Func fieldSelector) { if (data == null || !data.Any()) return 0; // 如果没有开业时间,或开业年份不是当前年份,按12个月计算 if (openTime == null || openTime.Value.Year != year) { return data.Sum(x => fieldSelector(x)) / 12; } // 如果是开业当年,排除开业当月 int openMonth = openTime.Value.Month; int validMonthCount = 12 - openMonth; if (validMonthCount <= 0) return 0; // 只统计开业月之后的数据 var validData = data.Where(x => x.Month > openMonth).ToList(); if (!validData.Any()) return 0; decimal total = validData.Sum(x => fieldSelector(x)); return total / validMonthCount; } /// /// 计算增长率(基于月均,考虑开业时间) /// private string CalculateGrowthRateByAverage( List currentData, List lastData, DateTime? openTime, int currentYear, Func fieldSelector) { // 25年新开门店,增长率 = 0 if (openTime != null && openTime.Value.Year == 2025) { return "0%"; } // 计算本年月均 decimal currentAvg = CalculateMonthlyAverage(currentData, openTime, currentYear, fieldSelector); // 计算上年月均 decimal lastAvg = CalculateMonthlyAverage(lastData, openTime, currentYear - 1, fieldSelector); if (lastAvg == 0) return currentAvg > 0 ? "100%" : "0%"; var rate = (currentAvg - lastAvg) / lastAvg; return (rate * 100).ToString("F2") + "%"; } ``` ### 步骤2:修改 `GetMonthlyStat` 方法 - 在方法开始处获取门店开业时间 - 修改月均计算逻辑(第1017行、1020行) - 修改增长率计算逻辑(第1022行) ### 步骤3:修改 `GetBusinessUnitSummaryStat` 方法 - 在方法开始处获取门店开业时间 - 为每个指标计算月均,然后基于月均计算增长率(第912-916行) ### 步骤4:修改 `GetStoreIndicatorStat` 方法 - 在方法开始处获取门店开业时间 - 修改增长率计算逻辑(第1116行) ### 步骤5:修改 `GetBuIndicatorStat` 方法 - 在方法开始处获取门店开业时间 - 修改增长率计算逻辑(第1195行) ## 五、注意事项 1. **数据一致性**:确保门店开业时间数据准确,如果 `kysj` 为空,按12个月计算 2. **边界情况**: - 开业月份 = 12月:有效月份数 = 0,月均 = 0 - 开业月份 = 1月:有效月份数 = 11,排除1月数据 3. **性能优化**:批量获取门店开业时间,避免在循环中查询数据库 4. **向后兼容**:保留原有的 `CalculateGrowthRate` 方法,新增基于月均的方法 ## 六、测试用例 ### 测试用例1:25年新开门店 - 门店:测试门店A - 开业时间:2025-06-01 - 本年业绩:100万(6-12月,7个月) - 上年业绩:0 - **预期结果**:增长率 = 0% ### 测试用例2:24年开业门店 - 门店:大源店 - 开业时间:2024-10-01 - 本年业绩:200万(全年12个月) - 上年业绩:50万(11月、12月,2个月) - 上年月均:50万 / 2 = 25万 - 本年月均:200万 / 12 = 16.67万 - **预期结果**:增长率 = (16.67 - 25) / 25 × 100% = -33.32% ### 测试用例3:正常门店 - 门店:正常门店B - 开业时间:2023-01-01 - 本年业绩:300万(全年12个月) - 上年业绩:240万(全年12个月) - 本年月均:300万 / 12 = 25万 - 上年月均:240万 / 12 = 20万 - **预期结果**:增长率 = (25 - 20) / 20 × 100% = 25%