年度经营统计分析增长率算法修改方案.md 9.07 KB

年度经营统计分析增长率算法修改方案

一、需求分析

当前算法

  • 增长率 = (本年总数 - 上年总数) / 上年总数 × 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 + 112 月的数据
  4. 月均 = 有效月份数据总和 / 有效月份数

对于25年新开门店(本年数据):

  1. 获取开业月份:openMonth = kysj.Month
  2. 计算有效月份数:validMonths = 12 - openMonth(排除开业当月)
  3. 计算有效月份数据总和:只统计 openMonth + 112 月的数据
  4. 月均 = 有效月份数据总和 / 有效月份数

对于其他门店:

  • 月均 = 总数 / 12

2.3 增长率计算逻辑

// 伪代码
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 月均计算方法

private decimal CalculateMonthlyAverage(
    List<LqAnnualSummaryEntity> 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<string, DateTime?> 格式

四、具体修改步骤

步骤1:新增辅助方法

/// <summary>
/// 批量获取门店开业时间
/// </summary>
private async Task<Dictionary<string, DateTime?>> GetStoreOpenTimesAsync(List<string> storeIds)
{
    var stores = await _db.Queryable<LqMdxxEntity>()
        .Where(x => storeIds.Contains(x.Id))
        .Select(x => new { x.Id, x.Kysj })
        .ToListAsync();

    return stores.ToDictionary(x => x.Id, x => x.Kysj);
}

/// <summary>
/// 计算月均(考虑开业时间)
/// </summary>
private decimal CalculateMonthlyAverage(
    List<LqAnnualSummaryEntity> data,
    DateTime? openTime,
    int year,
    Func<LqAnnualSummaryEntity, decimal> 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;
}

/// <summary>
/// 计算增长率(基于月均,考虑开业时间)
/// </summary>
private string CalculateGrowthRateByAverage(
    List<LqAnnualSummaryEntity> currentData,
    List<LqAnnualSummaryEntity> lastData,
    DateTime? openTime,
    int currentYear,
    Func<LqAnnualSummaryEntity, decimal> 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%