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 NCC.Extend.Entitys.lq_mdxx;
using NCC.Extend.Entitys.lq_hytk_hytk;
using NCC.Extend.Entitys.lq_hytk_jksyj;
using NCC.Extend.Entitys.lq_md_xdbhsj;
using NCC.Extend.Entitys.lq_salary_extra_calculation;
using NCC.System.Entitys.Permission;
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,
StoreId = x.StoreId,
StoreName = x.StoreName,
EmployeeId = x.EmployeeId,
EmployeeName = x.EmployeeName,
Position = x.Position,
GoldTriangleId = x.GoldTriangleId,
GoldTriangleTeam = x.GoldTriangleTeam,
TotalPerformance = x.TotalPerformance,
BasePerformance = x.BasePerformance,
CooperationPerformance = x.CooperationPerformance,
BaseRewardPerformance = x.BaseRewardPerformance,
CooperationRewardPerformance = x.CooperationRewardPerformance,
ActualBasePerformance = x.ActualBasePerformance,
ActualCooperationPerformance = x.ActualCooperationPerformance,
RewardPerformance = x.RewardPerformance,
StoreTotalPerformance = x.StoreTotalPerformance,
TeamPerformance = x.TeamPerformance,
Percentage = x.Percentage,
NewCustomerPerformance = x.NewCustomerPerformance,
NewCustomerConversionRate = x.NewCustomerConversionRate,
NewCustomerPoint = x.NewCustomerPoint,
UpgradeCustomerCount = x.UpgradeCustomerCount,
UpgradePerformance = x.UpgradePerformance,
UpgradePoint = x.UpgradePoint,
NewCustomerCommission = x.NewCustomerPerformanceCommission,
UpgradeCommission = x.UpgradePerformanceCommission,
OtherPerformanceAdd = x.OtherPerformanceAdd,
OtherPerformanceSubtract = x.OtherPerformanceSubtract,
Consumption = x.Consumption,
ProjectCount = x.ProjectCount,
CustomerCount = x.CustomerCount,
WorkingDays = x.WorkingDays,
LeaveDays = x.LeaveDays,
CommissionPoint = x.CommissionPoint,
BasePerformanceCommission = x.BasePerformanceCommission,
CooperationPerformanceCommission = x.CooperationPerformanceCommission,
ConsultantCommission = x.ConsultantCommission,
StoreTZoneCommission = x.StoreTZoneCommission,
TotalCommission = x.TotalCommission,
HealthCoachBaseSalary = x.HealthCoachBaseSalary,
HandworkFee = x.HandworkFee,
OutherHandworkFee = x.OutherHandworkFee,
TransportationAllowance = x.TransportationAllowance,
LessRest = x.LessRest,
FullAttendance = x.FullAttendance,
CalculatedGrossSalary = x.CalculatedGrossSalary,
GuaranteedSalary = x.GuaranteedSalary,
GuaranteedLeaveDeduction = x.GuaranteedLeaveDeduction,
GuaranteedBaseSalary = x.GuaranteedBaseSalary,
GuaranteedSupplement = x.GuaranteedSupplement,
FinalGrossSalary = x.FinalGrossSalary,
MonthlyTrainingSubsidy = x.MonthlyTrainingSubsidy,
MonthlyTransportSubsidy = x.MonthlyTransportSubsidy,
LastMonthTrainingSubsidy = x.LastMonthTrainingSubsidy,
LastMonthTransportSubsidy = x.LastMonthTransportSubsidy,
TotalSubsidy = x.TotalSubsidy,
MissingCard = x.MissingCard,
LateArrival = x.LateArrival,
LeaveDeduction = x.LeaveDeduction,
SocialInsuranceDeduction = x.SocialInsuranceDeduction,
RewardDeduction = x.RewardDeduction,
AccommodationDeduction = x.AccommodationDeduction,
StudyPeriodDeduction = x.StudyPeriodDeduction,
WorkClothesDeduction = x.WorkClothesDeduction,
TotalDeduction = x.TotalDeduction,
Bonus = x.Bonus,
ReturnPhoneDeposit = x.ReturnPhoneDeposit,
ReturnAccommodationDeposit = x.ReturnAccommodationDeposit,
ActualSalary = x.ActualSalary,
MonthlyPaymentStatus = x.MonthlyPaymentStatus,
PaidAmount = x.PaidAmount,
PendingAmount = x.PendingAmount,
LastMonthSupplement = x.LastMonthSupplement,
MonthlyTotalPayment = x.MonthlyTotalPayment,
StatisticsMonth = x.StatisticsMonth,
IsLocked = x.IsLocked,
CreateTime = x.CreateTime,
CreateUser = x.CreateUser,
UpdateTime = x.UpdateTime,
UpdateUser = x.UpdateUser,
IsNewStore = x.IsNewStore,
NewStoreProtectionStage = x.NewStoreProtectionStage,
StoreType = x.StoreType,
StoreCategory = x.StoreCategory,
DailyAverageConsumption = x.DailyAverageConsumption,
DailyAverageProjectCount = x.DailyAverageProjectCount,
TeamTotalConsumption = x.TeamTotalConsumption
})
.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) && x.Id != null)
.ToDictionaryAsync(x => x.Id, x => x.Sfskdd);
// 1.1.2 组合数据
var performanceData = performanceList.Select(p => new
{
Jks = p.Jkszh, // 使用 Jkszh (账号/ID) 而不是 Jks (姓名)
p.Jksxm,
p.StoreId,
p.Jksyj,
p.ItemCategory,
p.PerformanceType, // 新增业绩类型字段
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.3.1 退款数据 (lq_hytk_jksyj)
var refundList = await _db.Queryable()
.Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && 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.Where(x => !string.IsNullOrEmpty(x.Id)).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();
// 1.6.1 门店总业绩计算 (开单实付 - 退款金额)
// 开单实付
var storeBillingList = await _db.Queryable()
.Where(x => x.Kdrq >= startDate && x.Kdrq <= endDate.AddDays(1) && x.IsEffective == 1)
.Select(x => new { x.Djmd, x.Sfyj })
.ToListAsync();
var storeBillingDict = storeBillingList
.Where(x => !string.IsNullOrEmpty(x.Djmd))
.GroupBy(x => x.Djmd)
.ToDictionary(g => g.Key, g => g.Sum(x => x.Sfyj));
// 退款金额
var storeRefundList = await _db.Queryable()
.Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1)
.Select(x => new { x.Mdbh, x.Tkje, x.ActualRefundAmount })
.ToListAsync();
var storeRefundDict = storeRefundList
.Where(x => !string.IsNullOrEmpty(x.Mdbh))
.GroupBy(x => x.Mdbh)
.ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? 0));
// 1.7 门店信息 (lq_mdxx)
var storeList = await _db.Queryable().ToListAsync();
var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x);
// 1.7.1 门店新店保护信息 (lq_md_xdbhsj)
var newStoreProtectionList = await _db.Queryable()
.Where(x => x.Sfqy == 1)
.ToListAsync();
// 构造新店保护查找字典: StoreId -> List
// 因为一个门店可能有多个阶段配置,虽然通常是一个时间段,但为了严谨取符合当前月份的配置
// 逻辑: 统计月份 (startDate ~ endDate) 是否在 bhkssj ~ bhjssj 范围内
// 只要统计月份与保护期有交集,就算保护期。或者严格一点,统计月份的第一天在保护期内。
// 这里取: 统计月份的第一天 (startDate) 在保护期内
var newStoreProtectionDict = newStoreProtectionList
.Where(x => x.Bhkssj <= startDate && x.Bhjssj >= startDate)
.GroupBy(x => x.Mdid)
.ToDictionary(g => g.Key, g => g.First()); // 取第一个匹配的配置
// 1.8 健康师工资额外计算数据 (lq_salary_extra_calculation)
var extraCalculationList = await _db.Queryable()
.Where(x => x.Year == year && x.Month == month)
.ToListAsync();
var extraCalculationDict = extraCalculationList
.Where(x => !string.IsNullOrEmpty(x.EmployeeId))
.ToDictionary(x => x.EmployeeId, x => x);
// 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();
// 1.8 批量获取员工信息 (BASE_USER + BASE_POSITION)
// 使用 allEmployeeIds 作为驱动,查询 BASE_USER
// 重要:只统计岗位为"健康师"的员工
var userList = await _db.Queryable()
.Where(x => allEmployeeIds.Contains(x.Id)
&& x.Gw == "健康师" // 只统计岗位为"健康师"的员工
&& x.DeleteMark == null
&& x.EnabledMark == 1)
.Select(x => new { x.Id, x.RealName, x.PositionId, x.Mdid })
.ToListAsync();
// 过滤出健康师ID列表
var healthCoachIds = userList.Select(x => x.Id).ToList();
var userDict = userList.ToDictionary(x => x.Id, x => x);
var positionIds = userList.Select(x => x.PositionId).Distinct().ToList();
var positionList = await _db.Queryable().Where(x => positionIds.Contains(x.Id)).ToListAsync();
var positionLookup = positionList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x.FullName);
// 只处理健康师员工
foreach (var empId in allEmployeeIds.Where(x => healthCoachIds.Contains(x)))
{
var salary = new LqSalaryStatisticsEntity
{
Id = YitIdHelper.NextId().ToString(),
EmployeeId = empId,
StatisticsMonth = monthStr,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsLocked = 0
};
// 填充基础信息 (优先从 BASE_USER 获取)
string userMdid = null;
if (userDict.ContainsKey(empId))
{
var user = userDict[empId];
salary.EmployeeName = user.RealName;
userMdid = user.Mdid;
// 岗位
if (user.PositionId != null && positionLookup.ContainsKey(user.PositionId))
{
salary.Position = positionLookup[user.PositionId];
}
}
// 如果 BASE_USER 没名字,尝试从业务表获取
if (string.IsNullOrEmpty(salary.EmployeeName))
{
var perfRecord = performanceData.FirstOrDefault(x => x.Jks == empId);
var consRecord = consumptionList.FirstOrDefault(x => x.Jks == empId);
if (perfRecord != null) salary.EmployeeName = perfRecord.Jksxm;
else if (consRecord != null) salary.EmployeeName = consRecord.Jksxm;
}
// 填充门店ID (从业务数据获取,因为 User 表的 OrganizeId 未必是门店)
var perfStore = performanceData.FirstOrDefault(x => x.Jks == empId && !string.IsNullOrEmpty(x.StoreId));
var consStore = consumptionList.FirstOrDefault(x => x.Jks == empId && !string.IsNullOrEmpty(x.StoreId));
if (perfStore != null) salary.StoreId = perfStore.StoreId;
else if (consStore != null) salary.StoreId = consStore.StoreId;
// 如果业务数据没门店,尝试使用 User.Mdid
if (string.IsNullOrEmpty(salary.StoreId) && !string.IsNullOrEmpty(userMdid))
{
if (storeDict.ContainsKey(userMdid))
{
salary.StoreId = userMdid;
}
}
// 填充门店名称及分类信息
if (!string.IsNullOrEmpty(salary.StoreId) && storeDict.ContainsKey(salary.StoreId))
{
var store = storeDict[salary.StoreId];
salary.StoreName = store.Dm;
salary.StoreType = store.StoreType;
salary.StoreCategory = store.StoreCategory;
}
// 填充新店保护信息
if (!string.IsNullOrEmpty(salary.StoreId) && newStoreProtectionDict.ContainsKey(salary.StoreId))
{
var protection = newStoreProtectionDict[salary.StoreId];
salary.IsNewStore = "是";
salary.NewStoreProtectionStage = protection.Stage;
}
else
{
salary.IsNewStore = "否";
salary.NewStoreProtectionStage = 0;
}
// 2.1 计算个人业绩
var myPerf = performanceData.Where(x => x.Jks == empId).ToList();
salary.BasePerformance = myPerf.Where(x => (x.PerformanceType ?? "").Trim() == "基础业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0"));
salary.CooperationPerformance = myPerf.Where(x => (x.PerformanceType ?? "").Trim() == "合作业绩").Sum(x => decimal.Parse(x.Jksyj ?? "0"));
salary.TotalPerformance = myPerf.Sum(x => decimal.Parse(x.Jksyj ?? "0"));
// 扣除退款
var myRefunds = refundList.Where(x => x.Jks == empId).ToList();
if (myRefunds.Any())
{
decimal totalRefund = myRefunds.Sum(x => x.Jksyj ?? 0);
decimal baseRefund = myRefunds.Where(x => (x.PerformanceType ?? "").Trim() == "基础业绩").Sum(x => x.Jksyj ?? 0);
decimal cooperationRefund = myRefunds.Where(x => (x.PerformanceType ?? "").Trim() == "合作业绩").Sum(x => x.Jksyj ?? 0);
// 如果退款记录未标记类型,且总退款大于0,则可能需要处理(当前暂不处理未分类退款的明细扣除,仅扣除总额)
// 修正:如果PerformanceType为空,应当从总业绩扣除。若要从Base或Coop扣除,需确认业务规则。
// 假设:如果未分类,暂时不从Base/Coop扣(或者按比例扣?)。
// 根据现有数据,有部分是null。
// 安全起见,TotalPerformance 减去 totalRefund。
// Base 和 Coop 减去各自明确标识的部分。
salary.TotalPerformance -= totalRefund;
salary.BasePerformance -= baseRefund;
salary.CooperationPerformance -= cooperationRefund;
}
// 新客与升单业绩
salary.NewCustomerPerformance = myPerf.Where(x => string.Equals(x.Sfskdd, "是")).Sum(x => decimal.Parse(x.Jksyj ?? "0"));
salary.UpgradePerformance = myPerf.Where(x => string.Equals(x.Sfskdd, "否")).Sum(x => decimal.Parse(x.Jksyj ?? "0"));
// 2.1.1 填充额外计算数据
if (extraCalculationDict.ContainsKey(empId))
{
var extraData = extraCalculationDict[empId];
salary.BaseRewardPerformance = extraData.BaseRewardPerformance;
salary.CooperationRewardPerformance = extraData.CooperationRewardPerformance;
salary.OtherPerformanceAdd = extraData.OtherPerformanceAdd;
salary.OtherPerformanceSubtract = extraData.OtherPerformanceSubtract;
salary.UpgradeCustomerCount = extraData.UpgradeCustomerCount;
salary.NewCustomerConversionRate = extraData.NewCustomerConversionRate;
salary.NewCustomerPerformance = extraData.NewCustomerPerformance;
salary.UpgradePerformance = extraData.UpgradePerformance;
}
// 2.1.2 计算实际基础业绩和实际合作业绩
// 定义新店相关变量,供后续多处使用
bool isNewStore = salary.IsNewStore == "是";
int newStoreStage = salary.NewStoreProtectionStage;
// 实际基础业绩 = 基础业绩 - 基础奖励业绩 + 其他业绩加 - 其他业绩减
decimal actualBasePerformance = salary.BasePerformance
- salary.BaseRewardPerformance
+ salary.OtherPerformanceAdd
- salary.OtherPerformanceSubtract;
// 新店额外调整:根据阶段扣除新客业绩或升单业绩
if (isNewStore)
{
if (newStoreStage == 1)
{
// 第一阶段:扣除新客业绩
actualBasePerformance -= salary.NewCustomerPerformance;
}
else if (newStoreStage == 2)
{
// 第二阶段:扣除升单业绩
actualBasePerformance -= salary.UpgradePerformance;
}
}
salary.ActualBasePerformance = actualBasePerformance;
// 实际合作业绩 = 合作业绩 - 合作奖励业绩
salary.ActualCooperationPerformance = salary.CooperationPerformance - salary.CooperationRewardPerformance;
// 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;
// 计算日均消耗和日均项目数 (用于底薪计算)
// 逻辑: 总消耗/在店天数, 总项目数/在店天数
if (salary.WorkingDays > 0)
{
salary.DailyAverageConsumption = salary.Consumption / salary.WorkingDays;
salary.DailyAverageProjectCount = salary.ProjectCount / salary.WorkingDays;
}
else
{
salary.DailyAverageConsumption = 0;
salary.DailyAverageProjectCount = 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);
// 初始判断岗位:如果是战队队长(IsLeader=1)则是顾问,否则是健康师
// 注意:这里先根据战队设置判断,后续考勤不足21天会降级
if (myTeam != null && myTeam.IsLeader == 1)
{
salary.Position = "顾问";
}
else if (string.IsNullOrEmpty(salary.Position)) // 如果BASE_USER没岗位,且不是队长,默认为健康师
{
salary.Position = "健康师";
}
if (myTeam != null)
{
salary.GoldTriangleId = myTeam.TeamId;
salary.GoldTriangleTeam = myTeam.TeamName ?? "个人";
}
else
{
salary.GoldTriangleTeam = "个人";
}
// 2.6 门店总业绩
if (!string.IsNullOrEmpty(salary.StoreId))
{
decimal billing = storeBillingDict.ContainsKey(salary.StoreId) ? storeBillingDict[salary.StoreId] : 0;
decimal refund = storeRefundDict.ContainsKey(salary.StoreId) ? storeRefundDict[salary.StoreId] : 0;
salary.StoreTotalPerformance = billing - refund;
}
employeeStats[empId] = salary;
}
// 3. 处理战队逻辑 (考勤规则)
// 规则:若出勤天数 < 20天,则该健康师不计入战队,按单人计算。
// 按战队分组
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 >= 20)
{
validMembers.Add(member);
}
else
{
invalidMembers.Add(member);
}
}
// 对于无效成员,移除战队标识,视为单人,并重置岗位为健康师
foreach (var member in invalidMembers)
{
member.GoldTriangleId = null;
member.GoldTriangleTeam = "个人";
member.Position = "健康师"; // 降级为健康师
}
// 计算有效战队的总业绩和总消耗
var teamTotalPerformance = validMembers.Sum(x => x.TotalPerformance);
var teamTotalConsumption = validMembers.Sum(x => x.Consumption);
// 更新有效成员的战队业绩和战队总消耗
foreach (var member in validMembers)
{
member.TeamPerformance = teamTotalPerformance;
member.TeamTotalConsumption = teamTotalConsumption;
}
}
// 补充处理:对于没有战队ID的(个人,或被剔除的),战队业绩等于个人总业绩
foreach (var salary in employeeStats.Values)
{
if (string.IsNullOrEmpty(salary.GoldTriangleId))
{
salary.TeamPerformance = salary.TotalPerformance;
}
}
// 4. 计算薪资 (底薪 & 提成)
foreach (var salary in employeeStats.Values)
{
// 定义新店相关变量,供底薪和提成计算使用
bool isNewStore = salary.IsNewStore == "是";
int newStoreStage = salary.NewStoreProtectionStage;
// 4.1 底薪计算
// 4.1 底薪计算
// 传入当月天数用于计算标准日均
int daysInMonth = DateTime.DaysInMonth(year, month);
salary.HealthCoachBaseSalary = CalculateBaseSalary(
salary.DailyAverageConsumption,
salary.DailyAverageProjectCount,
daysInMonth,
salary.WorkingDays,
isNewStore);
// 4.2 提成计算
// 业绩门槛: 战队成员个人总业绩 <= 6000 无提成 (需按日均计算)
// 规则:战队成员日均业绩 <= 6000 / 当月天数 -> 无提成
decimal memberThreshold = 6000m;
if (daysInMonth > 0 && salary.WorkingDays > 0)
{
memberThreshold = (6000m / daysInMonth) * salary.WorkingDays;
}
if (!string.IsNullOrEmpty(salary.GoldTriangleId) && salary.TotalPerformance < memberThreshold) // 修正为小于校验
{
salary.TotalCommission = 0;
salary.BasePerformanceCommission = 0;
salary.CooperationPerformanceCommission = 0;
salary.ConsultantCommission = 0;
salary.NewCustomerPerformanceCommission = 0;
salary.UpgradePerformanceCommission = 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, daysInMonth, salary.WorkingDays);
}
else
{
// 单人 (或被剔除出战队)
// 注意:提成点按原始总业绩计算
// 单人按日均考核提成点
commissionPoint = GetTeamCommissionPoint(1, salary.TotalPerformance, daysInMonth, salary.WorkingDays);
}
salary.CommissionPoint = commissionPoint;
// 计算基础/合作提成(使用实际业绩)
salary.BasePerformanceCommission = salary.ActualBasePerformance * 0.95m * commissionPoint;
salary.CooperationPerformanceCommission = salary.ActualCooperationPerformance * 0.95m * 0.65m * commissionPoint;
// 计算新客转化率提成和升单人头提成(根据新店阶段)
// isNewStore 和 newStoreStage 已在上面定义
// 升单提点总是赋值(根据升单人头数)
decimal upgradeCommissionRate = GetUpgradeCommissionRate(salary.UpgradeCustomerCount);
salary.UpgradePoint = upgradeCommissionRate;
if (isNewStore)
{
if (newStoreStage == 1)
{
// 第一阶段:计算新客转化率提成 (需乘以0.95)
salary.NewCustomerPerformanceCommission = CalculateNewCustomerConversionCommission(
salary.NewCustomerPerformance,
salary.NewCustomerConversionRate) * 0.95m;
}
else if (newStoreStage == 2)
{
// 第二阶段:计算升单人头提成 (需乘以0.95)
salary.UpgradePerformanceCommission = CalculateUpgradeCustomerCommission(
salary.UpgradePerformance,
salary.UpgradeCustomerCount) * 0.95m;
}
// 第三阶段:不计算新客/升单提成
}
// 计算顾问提成
// 检查是否是顾问
// 注意:这里需要重新判断是否是顾问,因为可能被降级了
if (salary.Position == "顾问" && !string.IsNullOrEmpty(salary.GoldTriangleId))
{
// 获取战队人数
var teamMemberList = employeeStats.Values.Where(x => x.GoldTriangleId == salary.GoldTriangleId).ToList();
var teamMemberCount = teamMemberList.Count;
// 单人或者一个人的战队就没有顾问
if (teamMemberCount > 1)
{
salary.ConsultantCommission = CalculateConsultantCommission(
salary.TeamPerformance,
teamMemberList,
teamMemberCount,
isNewStore);
}
else
{
salary.ConsultantCommission = 0;
}
}
// 计算门店T区提成
// 规则:姓名包含"T区" -> 门店总业绩 * 0.05 * 0.05
if (!string.IsNullOrEmpty(salary.EmployeeName) && salary.EmployeeName.Contains("T区"))
{
salary.StoreTZoneCommission = salary.StoreTotalPerformance * 0.05m * 0.05m;
// T区人员仅核算提成,其他项(底薪、手工、社保等)归零
salary.HealthCoachBaseSalary = 0;
salary.HandworkFee = 0;
salary.BasePerformanceCommission = 0;
salary.CooperationPerformanceCommission = 0;
salary.ConsultantCommission = 0;
salary.NewCustomerPerformanceCommission = 0;
salary.UpgradePerformanceCommission = 0;
salary.TotalSubsidy = 0;
salary.TotalDeduction = 0;
}
salary.TotalCommission = salary.BasePerformanceCommission
+ salary.CooperationPerformanceCommission
+ salary.ConsultantCommission
+ salary.NewCustomerPerformanceCommission
+ salary.UpgradePerformanceCommission
+ salary.StoreTZoneCommission;
}
// 计算占比
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 dailyAvgConsumption, decimal dailyAvgProjectCount, int daysInMonth, decimal workingDays, bool isNewStore)
{
// 规则调整:按日均计算
// 一星:月消耗 10000 / 当月天数,项目数 96 / 当月天数
// 二星:月消耗 20000 / 当月天数,项目数 126 / 当月天数
// 三星:月消耗 40000 / 当月天数,项目数 156 / 当月天数
// 计算各星级日均标准
decimal consStar1 = 10000m / daysInMonth;
decimal consStar2 = 20000m / daysInMonth;
decimal consStar3 = 40000m / daysInMonth;
decimal projStar1 = 96m / daysInMonth;
decimal projStar2 = 126m / daysInMonth;
decimal projStar3 = 156m / daysInMonth;
// 0星:未达标 -> 1800
// 1星:>=1w标准 且 >=96个标准 -> 2000
// 2星:>=2w标准 且 >=126个标准 -> 2200
// 3星:>=4w标准 且 >=156个标准 -> 2400
// 特殊规则:若消耗或项目数中仅一项未达标(0星),底薪按1星(2000元)计算
// 新店规则:新店底薪最低为1星(2000元),不满足1星按1星算
int starCons = 0;
if (dailyAvgConsumption >= consStar3) starCons = 3;
else if (dailyAvgConsumption >= consStar2) starCons = 2;
else if (dailyAvgConsumption >= consStar1) starCons = 1;
int starProj = 0;
if (dailyAvgProjectCount >= projStar3) starProj = 3;
else if (dailyAvgProjectCount >= projStar2) starProj = 2;
else if (dailyAvgProjectCount >= projStar1) starProj = 1;
int finalStar = Math.Min(starCons, starProj);
// 特殊规则处理: 仅一项未达标(0星) -> 1星
if (finalStar == 0 && (starCons > 0 || starProj > 0))
{
finalStar = 1;
}
decimal baseSalary = finalStar switch
{
3 => 2400,
2 => 2200,
1 => 2000,
_ => 1800
};
// 新店保底1星(2000元)
if (isNewStore && baseSalary < 2000)
{
baseSalary = 2000;
}
// 最终计算:(底薪 / 当月天数) * 在店天数
if (daysInMonth > 0)
{
baseSalary = (baseSalary / daysInMonth) * workingDays;
}
return Math.Round(baseSalary, 2);
}
///
/// 获取战队提成点
///
private decimal GetTeamCommissionPoint(int memberCount, decimal teamPerformance, int daysInMonth, decimal workingDays)
{
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人
{
// 单人按照日均考核
decimal p1 = 60000m;
decimal p2 = 40000m;
decimal p3 = 20000m;
decimal p4 = 10000m;
if (daysInMonth > 0 && workingDays > 0)
{
p1 = (p1 / daysInMonth) * workingDays;
p2 = (p2 / daysInMonth) * workingDays;
p3 = (p3 / daysInMonth) * workingDays;
p4 = (p4 / daysInMonth) * workingDays;
}
if (teamPerformance >= p1) return 0.06m;
if (teamPerformance >= p2) return 0.05m;
if (teamPerformance >= p3) return 0.04m;
if (teamPerformance >= p4) return 0.03m;
}
return 0;
}
///
/// 计算顾问提成
///
/// 战队总业绩
/// 战队成员列表
/// 战队人数
/// 是否为新店
/// 顾问提成金额
private decimal CalculateConsultantCommission(decimal teamPerformance, List teamMembers, int teamMemberCount, bool isNewStore)
{
// 顾问提成规则:
// 战队人数3人:战队总业绩 ≥ 6万元 且 组员业绩达到40%以上 且 消耗达到6万元 → 团队总业绩0.8%
// 战队人数2人:战队总业绩 ≥ 4万元 且 组员业绩达到30%以上 且 消耗达到4万元 → 团队总业绩0.3%
// 如果3人以上,就按照3人的规则来
// 单人或者一个人的战队就没有顾问(已在调用处处理)
// 注意:
// 1. "组员业绩"指除顾问外的其他成员业绩总和
// 2. 只统计有效战队成员(考勤≥20天,未被剔除的成员)
// 3. "达到X%以上"指:组员业绩总和 ≥ 团队总业绩 × X%
// 4. 新店顾问不考核消耗
// 5. 消耗达标:3人战队整组消耗>=6万,2人战队整组消耗>=4万
// 使用传入的 teamMembers 计算总消耗
var teamConsumption = teamMembers.Sum(x => x.Consumption);
// 计算组员(非顾问)业绩总和
var memberPerformance = teamMembers.Where(x => x.Position != "顾问").Sum(x => x.TotalPerformance);
// 3人及以上战队:业绩≥6万 且 组员业绩≥40% 且 (新店 或 消耗≥6万) → 0.8%
if (teamMemberCount >= 3)
{
if (teamPerformance >= 60000 && memberPerformance >= teamPerformance * 0.4m)
{
if (isNewStore || teamConsumption >= 60000)
{
return teamPerformance * 0.008m;
}
}
}
// 2人战队:业绩≥4万 且 组员业绩≥30% 且 (新店 或 消耗≥4万) → 0.3%
else if (teamMemberCount == 2)
{
if (teamPerformance >= 40000 && memberPerformance >= teamPerformance * 0.3m)
{
if (isNewStore || teamConsumption >= 40000)
{
return teamPerformance * 0.003m;
}
}
}
// 1人战队:没有顾问,返回0(已在调用处处理,这里不会执行到)
return 0;
}
///
/// 计算新客转化率提成
///
private decimal CalculateNewCustomerConversionCommission(decimal newCustomerPerformance, decimal conversionRate)
{
decimal commissionRate = 0;
if (conversionRate >= 0.5m) commissionRate = 0.20m;
else if (conversionRate >= 0.45m) commissionRate = 0.15m;
else if (conversionRate >= 0.35m) commissionRate = 0.10m;
else if (conversionRate >= 0) commissionRate = 0.06m;
return newCustomerPerformance * commissionRate;
}
///
/// 获取升单提成比例
///
private decimal GetUpgradeCommissionRate(decimal upgradeCustomerCount)
{
if (upgradeCustomerCount >= 10) return 0.12m; // 大于等于10:12%
else if (upgradeCustomerCount >= 7 && upgradeCustomerCount < 10) return 0.10m; // 大于等于7且小于10:10%
else if (upgradeCustomerCount >= 4 && upgradeCustomerCount < 7) return 0.07m; // 大于等于4且小于7:7%
// 小于4:0%
return 0m;
}
///
/// 计算升单人头提成
///
private decimal CalculateUpgradeCustomerCommission(decimal upgradePerformance, decimal upgradeCustomerCount)
{
decimal commissionRate = GetUpgradeCommissionRate(upgradeCustomerCount);
return upgradePerformance * commissionRate;
}
}
}