using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NCC.Common.Enum;
using NCC.Common.Filter;
using NCC.Common.Helper;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.Dto.LqSalary;
using Yitter.IdGenerator;
using NCC.Extend.Entitys.lq_attendance_summary;
using NCC.Extend.Entitys.lq_jinsanjiao_user;
using NCC.Extend.Entitys.lq_kd_jksyj;
using NCC.Extend.Entitys.lq_kd_kdjlb;
using NCC.Extend.Entitys.lq_md_target;
using NCC.Extend.Entitys.lq_person_times_record;
using NCC.Extend.Entitys.lq_salary_statistics;
using NCC.Extend.Entitys.lq_xh_jksyj;
using NCC.Extend.Entitys.lq_ycsd_jsj;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NCC.Extend
{
///
/// 薪酬服务
///
[ApiDescriptionSettings(Tag = "薪酬服务", Name = "LqSalary", Order = 300)]
[Route("api/Extend/[controller]")]
public class LqSalaryService : IDynamicApiController, ITransient
{
private readonly ISqlSugarClient _db;
///
/// 初始化一个类型的新实例
///
public LqSalaryService(ISqlSugarClient db)
{
_db = db;
}
///
/// 获取健康师工资列表
///
/// 查询参数
/// 健康师工资分页列表
[HttpGet("health-coach")]
public async Task> GetHealthCoachSalaryList([FromQuery] HealthCoachSalaryInput input)
{
var monthStr = $"{input.Year}{input.Month:D2}";
// 1. 检查当月是否已生成工资数据
var exists = await _db.Queryable()
.AnyAsync(x => x.StatisticsMonth == monthStr);
// 2. 如果没有数据,则进行计算
if (!exists)
{
await CalculateHealthCoachSalary(input.Year, input.Month);
}
// 3. 查询数据
var query = _db.Queryable()
.Where(x => x.StatisticsMonth == monthStr);
if (!string.IsNullOrEmpty(input.StoreId))
{
query = query.Where(x => x.StoreId == input.StoreId);
}
if (!string.IsNullOrEmpty(input.Keyword))
{
query = query.Where(x => x.EmployeeName.Contains(input.Keyword) || x.EmployeeId.Contains(input.Keyword));
}
var list = await query.Select(x => new HealthCoachSalaryOutput
{
Id = x.Id,
StoreName = x.StoreName,
EmployeeName = x.EmployeeName,
Position = x.Position,
GoldTriangleTeam = x.GoldTriangleTeam,
TotalPerformance = x.TotalPerformance,
BasePerformance = x.BasePerformance,
CooperationPerformance = x.CooperationPerformance,
RewardPerformance = x.RewardPerformance,
Consumption = x.Consumption,
ProjectCount = x.ProjectCount,
CustomerCount = x.CustomerCount,
WorkingDays = x.WorkingDays,
HealthCoachBaseSalary = x.HealthCoachBaseSalary,
TotalCommission = x.TotalCommission,
HandworkFee = x.HandworkFee,
TotalSubsidy = x.TotalSubsidy,
TotalDeduction = x.TotalDeduction,
ActualSalary = x.ActualSalary,
IsLocked = x.IsLocked,
UpdateTime = x.UpdateTime
})
.ToPagedListAsync(input.currentPage, input.pageSize);
return PageResult.SqlSugarPageResult(list);
}
///
/// 计算健康师工资
///
/// 年份
/// 月份
///
[HttpPost("calculate/health-coach")]
public async Task CalculateHealthCoachSalary(int year, int month)
{
var startDate = new DateTime(year, month, 1);
var endDate = startDate.AddMonths(1).AddDays(-1);
var monthStr = $"{year}{month:D2}";
// 1. 获取基础数据
// 1.1 业绩数据 (lq_kd_jksyj)
var performanceList = await _db.Queryable()
.Where(x => x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1) && x.IsEffective == 1)
.ToListAsync();
// 1.1.1 获取关联的开单记录(用于获取 sfskdd)
var billingIds = performanceList.Select(x => x.Glkdbh).Distinct().ToList();
var billingDict = await _db.Queryable()
.Where(x => billingIds.Contains(x.Id))
.ToDictionaryAsync(x => x.Id, x => x.Sfskdd);
// 1.1.2 组合数据
var performanceData = performanceList.Select(p => new
{
p.Jks,
p.Jksxm,
p.StoreId,
p.Jksyj,
p.ItemCategory,
Sfskdd = billingDict.ContainsKey(p.Glkdbh) ? billingDict[p.Glkdbh] : null
}).ToList();
// 1.2 消耗数据 (lq_xh_jksyj)
var consumptionList = await _db.Queryable()
.Where(x => x.Yjsj >= startDate && x.Yjsj <= endDate.AddDays(1) && x.IsEffective == 1)
.ToListAsync();
// 1.3 考勤数据 (lq_attendance_summary)
var attendanceList = await _db.Queryable()
.Where(x => x.Year == year && x.Month == month && x.IsEffective == 1)
.ToListAsync();
// 1.4 战队成员及顾问信息 (lq_jinsanjiao_user + lq_ycsd_jsj)
var teamUserList = await _db.Queryable()
.Where(x => x.Month == monthStr && x.DeleteMark == 0)
.ToListAsync();
// 1.4.1 获取战队信息
var teamIds = teamUserList.Select(x => x.JsjId).Distinct().ToList();
var teamList = await _db.Queryable()
.Where(x => teamIds.Contains(x.Id))
.ToListAsync();
var teamDict = teamList.ToDictionary(x => x.Id, x => x.Jsj);
// 1.4.2 组合数据
var teamMembers = teamUserList.Select(user => new
{
user.UserId,
user.IsLeader,
TeamId = user.JsjId,
TeamName = teamDict.ContainsKey(user.JsjId) ? teamDict[user.JsjId] : (string)null
}).ToList();
// 1.5 到店人头 (lq_person_times_record)
// 统计每个健康师的去重会员数
var headcountList = await _db.Queryable()
.Where(x => x.WorkMonth == monthStr && x.IsEffective == 1)
.GroupBy(x => x.PersonId)
.Select(x => new { PersonId = x.PersonId, Count = SqlFunc.AggregateDistinctCount(x.MemberId) })
.ToListAsync();
// 1.6 门店生命线 (lq_md_target)
var storeTargets = await _db.Queryable()
.Where(x => x.Month == monthStr)
.ToListAsync();
// 2. 聚合每个健康师的数据对象
var employeeStats = new Dictionary();
// 获取所有涉及的健康师ID
var allEmployeeIds = performanceData.Select(x => x.Jks)
.Union(consumptionList.Select(x => x.Jks))
.Union(attendanceList.Select(x => x.UserId))
.Union(teamMembers.Select(x => x.UserId))
.Where(x => !string.IsNullOrEmpty(x))
.Distinct()
.ToList();
foreach (var empId in allEmployeeIds)
{
var salary = new LqSalaryStatisticsEntity
{
Id = YitIdHelper.NextId().ToString(),
EmployeeId = empId,
StatisticsMonth = monthStr,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsLocked = 0
};
// 填充基础信息 (姓名、门店)
var perfRecord = performanceData.FirstOrDefault(x => x.Jks == empId);
var consRecord = consumptionList.FirstOrDefault(x => x.Jks == empId);
if (perfRecord != null)
{
salary.EmployeeName = perfRecord.Jksxm;
salary.StoreId = perfRecord.StoreId;
}
else if (consRecord != null)
{
salary.EmployeeName = consRecord.Jksxm;
salary.StoreId = consRecord.StoreId;
}
// 填充门店名称
if (!string.IsNullOrEmpty(salary.StoreId))
{
// 这里简单处理,实际可能需要缓存门店列表
// salary.StoreName = ...
}
// 2.1 计算个人业绩
var myPerf = performanceData.Where(x => x.Jks == empId).ToList();
salary.BasePerformance = myPerf.Where(x => x.ItemCategory == "基础业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0"));
salary.CooperationPerformance = myPerf.Where(x => x.ItemCategory == "合作业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0"));
salary.TotalPerformance = myPerf.Sum(x => decimal.Parse(x.Jksyj ?? "0"));
// 新客与升单业绩
salary.NewCustomerPerformance = myPerf.Where(x => x.Sfskdd == "是").Sum(x => decimal.Parse(x.Jksyj ?? "0"));
salary.UpgradePerformance = myPerf.Where(x => x.Sfskdd == "否").Sum(x => decimal.Parse(x.Jksyj ?? "0"));
// 2.2 计算消耗和项目数
var myCons = consumptionList.Where(x => x.Jks == empId).ToList();
salary.Consumption = myCons.Sum(x => x.Jksyj ?? 0);
salary.ProjectCount = myCons.Sum(x => x.KdpxNumber ?? 0);
salary.HandworkFee = myCons.Sum(x => x.LaborCost ?? 0); // 使用 F_LaborCost
// 2.3 考勤数据
var myAtt = attendanceList.FirstOrDefault(x => x.UserId == empId);
salary.WorkingDays = myAtt?.WorkDays ?? 0;
salary.LeaveDays = myAtt?.LeaveDays ?? 0;
// 2.4 到店人头
var myHeadcount = headcountList.FirstOrDefault(x => x.PersonId == empId);
salary.CustomerCount = myHeadcount?.Count ?? 0;
// 2.5 战队信息 (初始)
var myTeam = teamMembers.FirstOrDefault(x => x.UserId == empId);
if (myTeam != null)
{
salary.GoldTriangleId = myTeam.TeamId;
salary.GoldTriangleTeam = myTeam.TeamName ?? "";
}
employeeStats[empId] = salary;
}
// 3. 处理战队逻辑 (考勤规则)
// 规则:若出勤天数 < 21天,则该健康师不计入战队,按单人计算。
// 按战队分组
var teamGroups = employeeStats.Values
.Where(x => !string.IsNullOrEmpty(x.GoldTriangleId))
.GroupBy(x => x.GoldTriangleId)
.ToList();
foreach (var group in teamGroups)
{
var validMembers = new List();
var invalidMembers = new List();
foreach (var member in group)
{
if (member.WorkingDays >= 21)
{
validMembers.Add(member);
}
else
{
invalidMembers.Add(member);
}
}
// 对于无效成员,移除战队标识,视为单人
foreach (var member in invalidMembers)
{
member.GoldTriangleId = null;
member.GoldTriangleTeam = null;
}
// 计算有效战队的总业绩
var teamTotalPerformance = validMembers.Sum(x => x.TotalPerformance);
// 更新有效成员的战队业绩
foreach (var member in validMembers)
{
member.TeamPerformance = teamTotalPerformance;
}
}
// 4. 计算薪资 (底薪 & 提成)
foreach (var salary in employeeStats.Values)
{
// 4.1 底薪计算
salary.HealthCoachBaseSalary = CalculateBaseSalary(salary.Consumption, salary.ProjectCount);
// 4.2 提成计算
// 单人业绩 <= 6000 无提成
if (salary.TotalPerformance <= 6000)
{
salary.TotalCommission = 0;
salary.BasePerformanceCommission = 0;
salary.CooperationPerformanceCommission = 0;
salary.ConsultantCommission = 0;
}
else
{
// 确定提成点
decimal commissionPoint = 0;
if (!string.IsNullOrEmpty(salary.GoldTriangleId))
{
// 是战队成员
// 获取战队人数 (注意:这里应该是有效战队人数)
var teamMemberCount = employeeStats.Values.Count(x => x.GoldTriangleId == salary.GoldTriangleId);
commissionPoint = GetTeamCommissionPoint(teamMemberCount, salary.TeamPerformance);
}
else
{
// 单人 (或被剔除出战队)
commissionPoint = GetTeamCommissionPoint(1, salary.TotalPerformance);
}
salary.CommissionPoint = commissionPoint;
// 计算基础/合作提成
salary.BasePerformanceCommission = salary.BasePerformance * 0.95m * commissionPoint;
salary.CooperationPerformanceCommission = salary.CooperationPerformance * 0.95m * 0.65m * commissionPoint;
// 计算顾问提成
// 检查是否是顾问
var isConsultant = teamMembers.Any(x => x.UserId == salary.EmployeeId && x.IsLeader == 1);
if (isConsultant && !string.IsNullOrEmpty(salary.GoldTriangleId))
{
salary.ConsultantCommission = CalculateConsultantCommission(salary.TeamPerformance, employeeStats.Values.Where(x => x.GoldTriangleId == salary.GoldTriangleId).ToList());
}
salary.TotalCommission = salary.BasePerformanceCommission + salary.CooperationPerformanceCommission + salary.ConsultantCommission;
}
// 计算占比
if (salary.TeamPerformance > 0)
{
salary.Percentage = salary.TotalPerformance / salary.TeamPerformance;
}
else if (salary.TotalPerformance > 0 && string.IsNullOrEmpty(salary.GoldTriangleId))
{
salary.Percentage = 1; // 单人占比100%
}
// 4.3 最终工资
salary.ActualSalary = salary.HealthCoachBaseSalary + salary.TotalCommission + salary.HandworkFee + salary.TotalSubsidy - salary.TotalDeduction;
}
// 5. 保存数据
if (employeeStats.Any())
{
// 先删除当月旧数据 (防止重复)
await _db.Deleteable().Where(x => x.StatisticsMonth == monthStr).ExecuteCommandAsync();
await _db.Insertable(employeeStats.Values.ToList()).ExecuteCommandAsync();
}
}
///
/// 计算底薪
///
private decimal CalculateBaseSalary(decimal consumption, decimal projectCount)
{
// 0星:<1w 或 <96个 -> 1800
// 1星:>=1w 且 >=96个 -> 2000
// 2星:>=2w 且 >=126个 -> 2200
// 3星:>=4w 且 >=156个 -> 2400
// 特殊规则:若消耗或项目数中仅一项未达标(0星),底薪按1星(2000元)计算
int starCons = 0;
if (consumption >= 40000) starCons = 3;
else if (consumption >= 20000) starCons = 2;
else if (consumption >= 10000) starCons = 1;
int starProj = 0;
if (projectCount >= 156) starProj = 3;
else if (projectCount >= 126) starProj = 2;
else if (projectCount >= 96) starProj = 1;
int finalStar = Math.Min(starCons, starProj);
// 特殊规则处理: 仅一项未达标(0星) -> 1星
if (finalStar == 0 && (starCons > 0 || starProj > 0))
{
finalStar = 1;
}
switch (finalStar)
{
case 3: return 2400;
case 2: return 2200;
case 1: return 2000;
default: return 1800;
}
}
///
/// 获取战队提成点
///
private decimal GetTeamCommissionPoint(int memberCount, decimal teamPerformance)
{
if (memberCount >= 3)
{
if (teamPerformance >= 150000) return 0.07m;
if (teamPerformance >= 120000) return 0.06m;
if (teamPerformance >= 90000) return 0.05m;
if (teamPerformance >= 60000) return 0.04m;
if (teamPerformance >= 30000) return 0.03m;
}
else if (memberCount == 2)
{
if (teamPerformance >= 80000) return 0.06m;
if (teamPerformance >= 60000) return 0.05m;
if (teamPerformance >= 40000) return 0.04m;
if (teamPerformance >= 20000) return 0.03m;
}
else // 1人
{
if (teamPerformance >= 60000) return 0.06m;
if (teamPerformance >= 40000) return 0.05m;
if (teamPerformance >= 20000) return 0.04m;
if (teamPerformance >= 10000) return 0.03m;
}
return 0;
}
///
/// 计算顾问提成
///
private decimal CalculateConsultantCommission(decimal teamPerformance, List teamMembers)
{
// 顾问提成规则:
// 高级顾问:战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8%
// 普通顾问:战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3%
// 这里的“组员业绩达到X%以上”理解为:除顾问外的成员业绩占比?或者每个成员都达标?
// 通常理解为:团队中是否有成员业绩贡献较高,或者团队整体结构健康。
// 假设“组员业绩达到X%”是指:团队中至少有一名成员(非顾问本人?)或者所有成员平均?
// 鉴于规则模糊,这里先简化实现:暂只考核总业绩和消耗。
// 消耗是团队总消耗吗?假设是。
var teamConsumption = teamMembers.Sum(x => x.Consumption);
// 高级顾问
if (teamPerformance >= 60000 && teamConsumption >= 60000)
{
return teamPerformance * 0.008m;
}
// 普通顾问
if (teamPerformance >= 40000 && teamConsumption >= 40000)
{
return teamPerformance * 0.003m;
}
return 0;
}
}
}