SalaryCalculationService.cs
25.2 KB
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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SqlSugar;
using LqSalaryCalculationService.Models;
using Yitter.IdGenerator;
namespace LqSalaryCalculationService.Services
{
/// <summary>
/// 工资核算服务实现
/// </summary>
public class SalaryCalculationService : ISalaryCalculationService
{
private readonly SqlSugarClient _db;
private readonly ILogger<SalaryCalculationService> _logger;
private readonly IConfiguration _configuration;
private readonly SalaryCalculationConfig _config;
public SalaryCalculationService(
SqlSugarClient db,
ILogger<SalaryCalculationService> logger,
IConfiguration configuration)
{
_db = db;
_logger = logger;
_configuration = configuration;
_config = _configuration.GetSection("SalaryCalculation").Get<SalaryCalculationConfig>() ?? new SalaryCalculationConfig();
}
/// <summary>
/// 初始化用户信息到工资表
/// </summary>
public async Task<UserInfoInitResult> InitializeUserInfoAsync(string calculationMonth)
{
try
{
_logger.LogInformation($"开始初始化用户信息,计算月份: {calculationMonth}");
// 1. 从base_user表获取所有用户基础信息(包括没有门店信息的)
var userInfoSql = @"
SELECT
u.F_Id as UserId,
u.F_REALNAME as UserName,
u.F_GW as Position,
u.F_MDID as StoreId,
COALESCE(m.dm, '无门店') as StoreName
FROM BASE_USER u
LEFT JOIN lq_mdxx m ON u.F_MDID = m.F_Id
WHERE u.F_ENABLEDMARK = 1
AND u.F_DELETEMARK IS NULL";
var userInfos = await _db.Ado.SqlQueryAsync<UserInfoDto>(userInfoSql);
_logger.LogInformation($"获取到 {userInfos.Count()} 名用户基础信息");
// 2. 清理工资表数据
_logger.LogInformation("开始清理工资表数据...");
var clearSql = "DELETE FROM lq_gz";
await _db.Ado.ExecuteCommandAsync(clearSql);
_logger.LogInformation("工资表数据清理完成");
// 3. 先插入所有用户到工资表
var insertCount = 0;
foreach (var userInfo in userInfos)
{
try
{
// 插入到lq_gz工资表 - 先插入基本字段
var insertSql = @"
INSERT INTO lq_gz (F_Id, userid, xm, hsgw, md, jsjzd)
VALUES (@fid, @userid, @xm, @hsgw, @md, @jsjzd)
ON DUPLICATE KEY UPDATE
xm = VALUES(xm),
hsgw = VALUES(hsgw),
md = VALUES(md)";
await _db.Ado.ExecuteCommandAsync(insertSql, new
{
fid = Guid.NewGuid().ToString(),
userid = userInfo.UserId,
xm = userInfo.UserName,
hsgw = userInfo.Position,
md = userInfo.StoreName,
jsjzd = "" // 先插入空值
});
insertCount++;
_logger.LogDebug($"已插入用户: {userInfo.UserName} - {userInfo.StoreName}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"插入用户 {userInfo.UserName} 时出错: {ex.Message}");
}
}
_logger.LogInformation($"第一阶段完成:已插入 {insertCount} 名用户到工资表");
// 4. 更新门店信息(重新查询有门店信息的用户)
var updateStoreCount = 0;
var storeUpdateSql = @"
SELECT
u.F_Id as UserId,
u.F_REALNAME as UserName,
m.dm as StoreName
FROM BASE_USER u
LEFT JOIN lq_mdxx m ON u.F_MDID = m.F_Id
WHERE u.F_ENABLEDMARK = 1
AND u.F_DELETEMARK IS NULL
AND u.F_MDID IS NOT NULL
AND m.dm IS NOT NULL";
var storeInfos = await _db.Ado.SqlQueryAsync<UserInfoDto>(storeUpdateSql);
foreach (var storeInfo in storeInfos)
{
try
{
var updateStoreSql = @"
UPDATE lq_gz
SET md = @storeName
WHERE userid = @userId";
await _db.Ado.ExecuteCommandAsync(updateStoreSql, new
{
storeName = storeInfo.StoreName,
userId = storeInfo.UserId
});
updateStoreCount++;
_logger.LogDebug($"已更新门店信息: {storeInfo.UserName} - {storeInfo.StoreName}");
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"更新门店信息 {storeInfo.UserName} 时出错: {ex.Message}");
}
}
_logger.LogInformation($"第二阶段完成:已更新 {updateStoreCount} 名用户的门店信息");
// 5. 更新金三角信息
var updateTeamCount = 0;
foreach (var userInfo in userInfos)
{
try
{
// 通过用户ID在lq_jinsanjiao_user中获取对应月份的金三角ID
var monthFormat = calculationMonth.Replace("-", "");
var teamSql = @"
SELECT jsj_user.jsj_id, jsj.jsj as TeamName
FROM lq_jinsanjiao_user jsj_user
LEFT JOIN lq_ycsd_jsj jsj ON jsj_user.jsj_id = jsj.F_Id
WHERE jsj_user.user_id = @userId
AND jsj_user.status = 'ACTIVE'
AND jsj_user.F_Month = @monthFormat
LIMIT 1";
var teamInfo = await _db.Ado.SqlQueryAsync<TeamInfoDto>(teamSql, new { userId = userInfo.UserId, monthFormat = monthFormat });
var teamName = teamInfo.FirstOrDefault()?.TeamName ?? "";
if (!string.IsNullOrEmpty(teamName))
{
var updateTeamSql = @"
UPDATE lq_gz
SET jsjzd = @teamName
WHERE userid = @userId";
await _db.Ado.ExecuteCommandAsync(updateTeamSql, new
{
teamName = teamName,
userId = userInfo.UserId
});
updateTeamCount++;
_logger.LogInformation($"已更新金三角信息: {userInfo.UserName} - {teamName}");
}
else
{
_logger.LogDebug($"用户 {userInfo.UserName} 没有找到金三角信息");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"更新金三角信息 {userInfo.UserName} 时出错: {ex.Message}");
}
}
_logger.LogInformation($"第三阶段完成:已更新 {updateTeamCount} 名用户的金三角信息");
_logger.LogInformation($"用户信息初始化完成,共处理 {insertCount} 名用户");
return new UserInfoInitResult
{
Success = true,
Message = "用户信息初始化完成",
UserCount = insertCount,
CalculationMonth = calculationMonth
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"初始化用户信息失败: {ex.Message}");
return new UserInfoInitResult
{
Success = false,
Message = $"初始化用户信息失败: {ex.Message}",
CalculationMonth = calculationMonth
};
}
}
/// <summary>
/// 执行工资核算
/// </summary>
public async Task<SalaryCalculationResult> CalculateSalaryAsync(string calculationMonth)
{
try
{
_logger.LogInformation($"开始执行工资核算,计算月份: {calculationMonth}");
// 1. 获取员工业绩数据
var performances = await GetEmployeePerformanceAsync(calculationMonth);
_logger.LogInformation($"获取到 {performances.Count} 条员工业绩数据");
// 2. 计算每个员工的工资
var salaries = new List<EmployeeSalary>();
foreach (var performance in performances)
{
EmployeeSalary salary;
switch (performance.PositionCategory)
{
case "健康师":
salary = await CalculateHealthWorkerSalaryAsync(performance);
break;
case "店长":
case "主任":
case "店助":
salary = await CalculateManagerSalaryAsync(performance);
break;
default:
salary = await CalculateDefaultSalaryAsync(performance);
break;
}
salaries.Add(salary);
}
// 3. 导出工资报表
var outputPath = Path.Combine(_config.OutputPath, $"工资核算_{calculationMonth}.xlsx");
var exportPath = await ExportSalaryReportAsync(salaries, outputPath);
// 4. 发送邮件通知
if (_config.EnableEmailNotification)
{
await SendEmailNotificationAsync(new SalaryCalculationResult
{
Success = true,
Message = "工资核算完成",
Salaries = salaries,
OutputFilePath = exportPath,
CalculationMonth = calculationMonth
});
}
_logger.LogInformation($"工资核算完成,共计算 {salaries.Count} 名员工工资");
return new SalaryCalculationResult
{
Success = true,
Message = "工资核算完成",
Salaries = salaries,
OutputFilePath = exportPath,
CalculationMonth = calculationMonth
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"工资核算失败: {ex.Message}");
return new SalaryCalculationResult
{
Success = false,
Message = $"工资核算失败: {ex.Message}",
CalculationMonth = calculationMonth
};
}
}
/// <summary>
/// 获取员工业绩数据
/// </summary>
public async Task<List<EmployeePerformance>> GetEmployeePerformanceAsync(string calculationMonth)
{
var startDate = DateTime.Parse($"{calculationMonth}-01");
var endDate = startDate.AddMonths(1).AddDays(-1);
var sql = @"
SELECT
u.F_Id as EmployeeId,
u.F_REALNAME as EmployeeName,
u.F_MDID as StoreId,
m.dm as StoreName,
u.F_GW as Position,
u.F_GWFL as PositionCategory,
COALESCE(SUM(CAST(yj.ssyj as DECIMAL(18,2))), 0) as TotalPerformance,
COALESCE(SUM(CAST(xh.ssyj as DECIMAL(18,2))), 0) as ConsumptionPerformance,
COUNT(DISTINCT yj.xmbh) as ProjectCount,
COUNT(DISTINCT yj.khbh) as CustomerCount,
COALESCE(SUM(CAST(jsj.team_performance as DECIMAL(18,2))), 0) as TeamPerformance,
CASE WHEN m.kysj >= DATE_SUB(NOW(), INTERVAL 6 MONTH) THEN 1 ELSE 0 END as IsNewStore,
COALESCE(m.xsyj, 0) as StoreLifeLine,
COALESCE(jsj.team_id, '') as TeamId,
COALESCE(jsj.team_member_count, 1) as TeamMemberCount
FROM BASE_USER u
LEFT JOIN lq_mdxx m ON u.F_MDID = m.F_Id
LEFT JOIN lq_yjmxb yj ON u.F_REALNAME = yj.jks
AND DATE_FORMAT(yj.fssj, '%Y-%m') = @month
LEFT JOIN lq_xhmxb xh ON u.F_REALNAME = xh.jks
AND DATE_FORMAT(xh.fssj, '%Y-%m') = @month
LEFT JOIN (
SELECT
jsj.F_Id as team_id,
jsj.jsj,
COUNT(jsj_user.user_id) as team_member_count,
SUM(CAST(yj2.ssyj as DECIMAL(18,2))) as team_performance
FROM lq_ycsd_jsj jsj
LEFT JOIN lq_jinsanjiao_user jsj_user ON jsj.F_Id = jsj_user.jsj_id
LEFT JOIN lq_yjmxb yj2 ON jsj_user.user_name = yj2.jks
AND DATE_FORMAT(yj2.fssj, '%Y-%m') = @month
WHERE jsj.yf = @month
GROUP BY jsj.F_Id, jsj.jsj
) jsj ON u.F_REALNAME = jsj.jsj
WHERE u.F_ENABLEDMARK = 1
AND u.F_DELETEMARK IS NULL
GROUP BY u.F_Id, u.F_REALNAME, u.F_MDID, m.dm, u.F_GW, u.F_GWFL,
m.kysj, m.xsyj, jsj.team_id, jsj.team_member_count";
var performances = await _db.Ado.SqlQueryAsync<EmployeePerformance>(sql, new { month = calculationMonth });
return performances.ToList();
}
/// <summary>
/// 计算健康师工资
/// </summary>
public Task<EmployeeSalary> CalculateHealthWorkerSalaryAsync(EmployeePerformance performance)
{
var salary = new EmployeeSalary
{
Id = Guid.NewGuid().ToString(),
EmployeeId = performance.EmployeeId,
EmployeeName = performance.EmployeeName,
StoreId = performance.StoreId,
StoreName = performance.StoreName,
Position = performance.Position,
PositionCategory = performance.PositionCategory,
CalculationMonth = performance.StoreId, // 这里应该传计算月份
TotalPerformance = performance.TotalPerformance,
ConsumptionPerformance = performance.ConsumptionPerformance,
ProjectCount = performance.ProjectCount,
CustomerCount = performance.CustomerCount,
TeamPerformance = performance.TeamPerformance,
IsNewStore = performance.IsNewStore,
StoreLifeLine = performance.StoreLifeLine,
CreatedTime = DateTime.Now
};
// 根据健康师薪酬规则计算底薪
if (performance.ConsumptionPerformance >= 40000 && performance.ProjectCount >= 156)
{
salary.BaseSalary = 2400; // 三星
}
else if (performance.ConsumptionPerformance >= 20000 && performance.ProjectCount >= 126)
{
salary.BaseSalary = 2200; // 二星
}
else if (performance.ConsumptionPerformance >= 10000 && performance.ProjectCount >= 96)
{
salary.BaseSalary = 2000; // 一星
}
else
{
salary.BaseSalary = 1800; // 0星
}
// 计算金三角提成
if (performance.TeamMemberCount >= 1)
{
var commissionRate = CalculateTeamCommissionRate(performance.TeamPerformance, performance.TeamMemberCount);
salary.TeamCommission = performance.TeamPerformance * commissionRate / 100;
}
// 计算个人提成(基础业绩提成)
if (performance.TotalPerformance > 6000)
{
salary.CommissionAmount = performance.TotalPerformance * 0.95m * 0.03m; // 3%提成点
}
salary.GrossSalary = salary.BaseSalary + salary.CommissionAmount + salary.TeamCommission + salary.BonusAmount - salary.DeductionAmount;
salary.NetSalary = salary.GrossSalary;
return Task.FromResult(salary);
}
/// <summary>
/// 计算金三角提成比例
/// </summary>
public decimal CalculateTeamCommissionRate(decimal teamPerformance, int memberCount)
{
switch (memberCount)
{
case 3: // 3人战队
if (teamPerformance >= 150000) return 7m;
if (teamPerformance >= 120000) return 6m;
if (teamPerformance >= 90000) return 5m;
if (teamPerformance >= 60000) return 4m;
if (teamPerformance >= 30000) return 3m;
break;
case 2: // 2人战队
if (teamPerformance >= 80000) return 6m;
if (teamPerformance >= 60000) return 5m;
if (teamPerformance >= 40000) return 4m;
if (teamPerformance >= 20000) return 3m;
break;
case 1: // 1人战队
if (teamPerformance >= 60000) return 6m;
if (teamPerformance >= 40000) return 5m;
if (teamPerformance >= 20000) return 4m;
if (teamPerformance >= 10000) return 3m;
break;
}
return 0m;
}
/// <summary>
/// 计算门店管理员工资
/// </summary>
public Task<EmployeeSalary> CalculateManagerSalaryAsync(EmployeePerformance performance)
{
var salary = new EmployeeSalary
{
Id = Guid.NewGuid().ToString(),
EmployeeId = performance.EmployeeId,
EmployeeName = performance.EmployeeName,
StoreId = performance.StoreId,
StoreName = performance.StoreName,
Position = performance.Position,
PositionCategory = performance.PositionCategory,
CalculationMonth = performance.StoreId, // 这里应该传计算月份
TotalPerformance = performance.TotalPerformance,
ConsumptionPerformance = performance.ConsumptionPerformance,
ProjectCount = performance.ProjectCount,
CustomerCount = performance.CustomerCount,
TeamPerformance = performance.TeamPerformance,
IsNewStore = performance.IsNewStore,
StoreLifeLine = performance.StoreLifeLine,
CreatedTime = DateTime.Now
};
// 根据职位设置底薪
switch (performance.Position)
{
case "店长":
salary.BaseSalary = 4000;
break;
case "主任":
salary.BaseSalary = 3500;
break;
case "店助":
salary.BaseSalary = 3000;
break;
default:
salary.BaseSalary = 3000;
break;
}
// 计算提成(基于毛利)
var grossProfit = performance.TotalPerformance * 0.6m; // 假设毛利率60%
var commissionRate = 0.03m; // 3%提成率
salary.CommissionAmount = grossProfit * commissionRate;
// 检查是否达标
salary.IsTargetAchieved = performance.TotalPerformance >= performance.StoreLifeLine;
salary.GrossSalary = salary.BaseSalary + salary.CommissionAmount + salary.BonusAmount - salary.DeductionAmount;
salary.NetSalary = salary.GrossSalary;
return Task.FromResult(salary);
}
/// <summary>
/// 计算默认工资
/// </summary>
private Task<EmployeeSalary> CalculateDefaultSalaryAsync(EmployeePerformance performance)
{
var salary = new EmployeeSalary
{
Id = Guid.NewGuid().ToString(),
EmployeeId = performance.EmployeeId,
EmployeeName = performance.EmployeeName,
StoreId = performance.StoreId,
StoreName = performance.StoreName,
Position = performance.Position,
PositionCategory = performance.PositionCategory,
CalculationMonth = performance.StoreId,
TotalPerformance = performance.TotalPerformance,
ConsumptionPerformance = performance.ConsumptionPerformance,
ProjectCount = performance.ProjectCount,
CustomerCount = performance.CustomerCount,
TeamPerformance = performance.TeamPerformance,
IsNewStore = performance.IsNewStore,
StoreLifeLine = performance.StoreLifeLine,
CreatedTime = DateTime.Now,
BaseSalary = 2000, // 默认底薪
GrossSalary = 2000,
NetSalary = 2000
};
return Task.FromResult(salary);
}
/// <summary>
/// 导出工资报表
/// </summary>
public async Task<string> ExportSalaryReportAsync(List<EmployeeSalary> salaries, string outputPath)
{
try
{
// 确保输出目录存在
var directory = Path.GetDirectoryName(outputPath);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory!);
}
// 这里可以使用EPPlus或其他Excel库来生成Excel文件
// 为了简化,这里生成CSV文件
var csvPath = outputPath.Replace(".xlsx", ".csv");
using var writer = new StreamWriter(csvPath);
await writer.WriteLineAsync("员工姓名,门店名称,职位,底薪,总业绩,消耗业绩,提成金额,应发工资,实发工资,计算月份");
foreach (var salary in salaries)
{
await writer.WriteLineAsync($"{salary.EmployeeName},{salary.StoreName},{salary.Position}," +
$"{salary.BaseSalary},{salary.TotalPerformance},{salary.ConsumptionPerformance}," +
$"{salary.CommissionAmount},{salary.GrossSalary},{salary.NetSalary},{salary.CalculationMonth}");
}
_logger.LogInformation($"工资报表已导出到: {csvPath}");
return csvPath;
}
catch (Exception ex)
{
_logger.LogError(ex, $"导出工资报表失败: {ex.Message}");
throw;
}
}
/// <summary>
/// 发送邮件通知
/// </summary>
public Task<bool> SendEmailNotificationAsync(SalaryCalculationResult result)
{
try
{
// 这里可以实现邮件发送逻辑
// 可以使用MailKit或其他邮件库
_logger.LogInformation($"邮件通知功能待实现,核算结果: {result.Message}");
return Task.FromResult(true);
}
catch (Exception ex)
{
_logger.LogError(ex, $"发送邮件通知失败: {ex.Message}");
return Task.FromResult(false);
}
}
}
/// <summary>
/// 用户信息DTO
/// </summary>
public class UserInfoDto
{
public string UserId { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
public string Position { get; set; } = string.Empty;
public string StoreId { get; set; } = string.Empty;
public string StoreName { get; set; } = string.Empty;
}
/// <summary>
/// 团队信息DTO
/// </summary>
public class TeamInfoDto
{
public string JsjId { get; set; } = string.Empty;
public string TeamName { get; set; } = string.Empty;
}
}