Blame view

项目文档相关/docs/年度经营统计分析增长率算法修改方案.md 9.07 KB
3ca01a2d   “wangming”   feat: 更新多个文件和接口,优...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
  # 年度经营统计分析增长率算法修改方案
  
  ## 一、需求分析
  
  ### 当前算法
  - 增长率 = (本年总数 - 上年总数) / 上年总数 × 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<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:新增辅助方法
  
  ```csharp
  /// <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%