using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NCC.Common.Enum;
using NCC.Common.Filter;
using NCC.Common.Helper;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.Dto.LqBusinessUnitManagerSalary;
using NCC.Extend.Entitys.Dto.LqSalary;
using NCC.Extend.Entitys.Enum;
using NCC.Extend.Entitys.lq_attendance_summary;
using NCC.Extend.Entitys.lq_cooperation_cost;
using NCC.Extend.Entitys.lq_hytk_hytk;
using NCC.Extend.Entitys.lq_kd_kdjlb;
using NCC.Extend.Entitys.lq_laundry_flow;
using NCC.Extend.Entitys.lq_md_general_manager_lifeline;
using NCC.Extend.Entitys.lq_md_target;
using NCC.Extend.Entitys.lq_mdxx;
using NCC.Extend.Entitys.lq_business_unit_manager_salary_statistics;
using NCC.Extend.Entitys.lq_store_expense;
using NCC.FriendlyException;
using NCC.System.Entitys.Permission;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Yitter.IdGenerator;
using Microsoft.AspNetCore.Http;
namespace NCC.Extend
{
///
/// 事业部总经理/经理薪酬服务
///
[ApiDescriptionSettings(Tag = "事业部总经理/经理薪酬服务", Name = "LqBusinessUnitManagerSalary", Order = 304)]
[Route("api/Extend/[controller]")]
public class LqBusinessUnitManagerSalaryService : IDynamicApiController, ITransient
{
private readonly ISqlSugarClient _db;
private readonly ILogger _logger;
///
/// 初始化一个类型的新实例
///
public LqBusinessUnitManagerSalaryService(ISqlSugarClient db, ILogger logger)
{
_db = db;
_logger = logger;
}
///
/// 获取事业部总经理/经理工资列表
///
/// 查询参数
/// 事业部总经理/经理工资分页列表
[HttpGet("business-unit-manager")]
public async Task GetBusinessUnitManagerSalaryList([FromQuery] BusinessUnitManagerSalaryInput input)
{
var monthStr = $"{input.Year}{input.Month:D2}";
// 查询数据
var query = _db.Queryable()
.Where(x => x.StatisticsMonth == monthStr);
if (input.ManagerType.HasValue)
{
query = query.Where(x => x.ManagerType == input.ManagerType.Value);
}
if (!string.IsNullOrEmpty(input.Keyword))
{
query = query.Where(x => x.EmployeeName.Contains(input.Keyword) || x.EmployeeAccount.Contains(input.Keyword));
}
var list = await query.Select(x => new BusinessUnitManagerSalaryOutput
{
Id = x.Id,
StatisticsMonth = x.StatisticsMonth,
Position = x.Position,
EmployeeName = x.EmployeeName,
EmployeeId = x.EmployeeId,
EmployeeAccount = x.EmployeeAccount,
ManagerType = x.ManagerType,
IsTerminated = x.IsTerminated,
StorePerformanceDetail = x.StorePerformanceDetail,
SalesPerformance = x.SalesPerformance,
ProductMaterial = x.ProductMaterial,
CooperationCost = x.CooperationCost,
StoreExpense = x.StoreExpense,
LaundryCost = x.LaundryCost,
GrossProfit = x.GrossProfit,
BaseSalary = x.BaseSalary,
TotalCommission = x.TotalCommission,
WorkingDays = x.WorkingDays,
LeaveDays = x.LeaveDays,
CalculatedGrossSalary = x.CalculatedGrossSalary,
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,
IsLocked = x.IsLocked,
EmployeeConfirmStatus = x.EmployeeConfirmStatus,
UpdateTime = x.UpdateTime
})
.ToPagedListAsync(input.currentPage, input.pageSize);
return PageResult.SqlSugarPageResult(list);
}
///
/// 通过月份和员工ID查询工资(仅查询已锁定且未确认的工资)
///
///
/// **重要**:此接口只能查询已锁定(IsLocked=1)且未确认(EmployeeConfirmStatus=0或null)的工资记录
/// **注意**:已确认的工资记录无法通过此接口查询
///
[HttpGet("query-by-employee")]
public async Task GetSalaryByEmployee([FromQuery] SalaryQueryByEmployeeInput input)
{
if (input.Year <= 0 || input.Month <= 0 || input.Month > 12)
throw NCCException.Oh("年份和月份参数不正确");
if (string.IsNullOrWhiteSpace(input.EmployeeId))
throw NCCException.Oh("员工ID不能为空");
var monthStr = $"{input.Year}{input.Month:D2}";
var salary = await _db.Queryable()
.Where(x => x.StatisticsMonth == monthStr && x.EmployeeId == input.EmployeeId && x.IsLocked == 1 && x.EmployeeConfirmStatus != 1)
.Select(x => new BusinessUnitManagerSalaryOutput
{
Id = x.Id,
StatisticsMonth = x.StatisticsMonth,
Position = x.Position,
EmployeeName = x.EmployeeName,
EmployeeId = x.EmployeeId,
EmployeeAccount = x.EmployeeAccount,
ManagerType = x.ManagerType,
IsTerminated = x.IsTerminated,
StorePerformanceDetail = x.StorePerformanceDetail,
SalesPerformance = x.SalesPerformance,
ProductMaterial = x.ProductMaterial,
CooperationCost = x.CooperationCost,
StoreExpense = x.StoreExpense,
LaundryCost = x.LaundryCost,
GrossProfit = x.GrossProfit,
BaseSalary = x.BaseSalary,
TotalCommission = x.TotalCommission,
WorkingDays = x.WorkingDays,
LeaveDays = x.LeaveDays,
CalculatedGrossSalary = x.CalculatedGrossSalary,
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,
IsLocked = x.IsLocked,
EmployeeConfirmStatus = x.EmployeeConfirmStatus,
UpdateTime = x.UpdateTime
})
.FirstAsync();
if (salary == null)
throw NCCException.Oh($"未找到员工{input.EmployeeId}在{input.Year}年{input.Month}月的工资记录");
return salary;
}
///
/// 计算事业部总经理/经理工资
///
/// 年份
/// 月份
///
[HttpPost("calculate/business-unit-manager")]
public async Task CalculateBusinessUnitManagerSalary(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_md_general_manager_lifeline表)
var lifelineList = await _db.Queryable()
.Where(x => x.Month == monthStr)
.ToListAsync();
if (!lifelineList.Any())
{
// 如果没有归属信息,直接返回
return;
}
// 1.2 获取所有不重复的总经理/经理ID(确保所有总经理/经理都被计算)
var allManagerIds = lifelineList
.Where(x => !string.IsNullOrEmpty(x.GeneralManagerId))
.Select(x => x.GeneralManagerId)
.Distinct()
.ToList();
// 1.3 按总经理/经理ID分组,获取每个总经理/经理管理的门店
var managerStoreDict = lifelineList
.Where(x => !string.IsNullOrEmpty(x.GeneralManagerId) && !string.IsNullOrEmpty(x.StoreId))
.GroupBy(x => x.GeneralManagerId)
.ToDictionary(g => g.Key, g => g.Select(x => x.StoreId).Distinct().ToList());
// 1.4 门店信息 (lq_mdxx)
var storeList = await _db.Queryable().ToListAsync();
var storeDict = storeList.Where(x => !string.IsNullOrEmpty(x.Id)).ToDictionary(x => x.Id, x => x);
// 1.5 门店生命线信息 (lq_md_target)
var targetList = await _db.Queryable()
.Where(x => x.Month == monthStr)
.ToListAsync();
var storeLifelineDict = targetList
.Where(x => !string.IsNullOrEmpty(x.StoreId))
.ToDictionary(x => x.StoreId, x => x.StoreLifeline);
// 1.6 门店销售业绩计算 (开单实付 - 退卡金额)
// 开单实付(从lq_kd_kdjlb表统计sfyj字段)
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));
// 退卡金额(从lq_hytk_hytk表统计,使用F_ActualRefundAmount,如果没有则使用tkje)
var storeRefundList = await _db.Queryable()
.Where(x => x.Tksj >= startDate && x.Tksj <= endDate.AddDays(1) && x.IsEffective == 1)
.Select(x => new { x.Md, x.ActualRefundAmount, x.Tkje })
.ToListAsync();
var storeRefundDict = storeRefundList
.Where(x => !string.IsNullOrEmpty(x.Md))
.GroupBy(x => x.Md)
.ToDictionary(g => g.Key, g => g.Sum(x => x.ActualRefundAmount ?? x.Tkje ?? 0));
// 1.7 产品物料统计(仓库领用金额,使用上月数据,基于领取时间)
// 计算本月工资时,使用上月数据(如:计算12月工资,使用11月数据)
// 只统计已审批通过且已领取的记录
var queryMonth = monthStr;
if (month == 1)
{
// 1月工资使用上一年12月数据
queryMonth = $"{year - 1}12";
}
else
{
// 其他月份使用上一个月数据
queryMonth = $"{year}{(month - 1):D2}";
}
var productMaterialSql = $@"
SELECT
u.F_StoreId as StoreId,
COALESCE(SUM(u.F_TotalAmount), 0) as MaterialAmount
FROM lq_inventory_usage u
INNER JOIN lq_inventory_usage_application a ON u.F_UsageBatchId = a.F_UsageBatchId
WHERE u.F_IsEffective = 1
AND a.F_IsEffective = 1
AND a.F_ApprovalStatus = '已通过'
AND a.F_IsReceived = 1
AND a.F_ReceiveTime IS NOT NULL
AND DATE_FORMAT(a.F_ReceiveTime, '%Y%m') = @queryMonth
GROUP BY u.F_StoreId";
var productMaterialData = await _db.Ado.SqlQueryAsync(productMaterialSql, new { queryMonth });
var productMaterialDict = productMaterialData
.Where(x => x.StoreId != null)
.ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.MaterialAmount ?? 0));
// 1.8 合作项目成本统计
// Month字段格式为"11"(月份数字),不是"202511"(YYYYMM格式)
var cooperationCostMonth = $"{month:D2}"; // 格式化为"11"
var cooperationCostList = await _db.Queryable()
.Where(x => x.Year == year && x.Month == cooperationCostMonth && x.IsEffective == StatusEnum.有效.GetHashCode())
.Select(x => new { x.StoreId, x.TotalAmount })
.ToListAsync();
var cooperationCostDict = cooperationCostList
.Where(x => !string.IsNullOrEmpty(x.StoreId))
.GroupBy(x => x.StoreId)
.ToDictionary(g => g.Key, g => g.Sum(x => x.TotalAmount));
// 1.9 店内支出统计
var storeExpenseSql = $@"
SELECT
F_StoreId as StoreId,
COALESCE(SUM(F_Amount), 0) as ExpenseAmount
FROM lq_store_expense
WHERE F_IsEffective = 1
AND DATE_FORMAT(F_ExpenseDate, '%Y%m') = @monthStr
GROUP BY F_StoreId";
var storeExpenseData = await _db.Ado.SqlQueryAsync(storeExpenseSql, new { monthStr });
var storeExpenseDict = storeExpenseData
.Where(x => x.StoreId != null)
.ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.ExpenseAmount ?? 0));
// 1.10 洗毛巾费用统计(只统计送出的记录,F_FlowType = 0)
// 优先使用送出时间(F_SendTime),如果为空则使用创建时间(F_CreateTime)
var laundryCostSql = $@"
SELECT
F_StoreId as StoreId,
COALESCE(SUM(F_TotalPrice), 0) as LaundryAmount
FROM lq_laundry_flow
WHERE F_IsEffective = 1
AND F_FlowType = 0
AND DATE_FORMAT(COALESCE(F_SendTime, F_CreateTime), '%Y%m') = @monthStr
GROUP BY F_StoreId";
var laundryCostData = await _db.Ado.SqlQueryAsync(laundryCostSql, new { monthStr });
var laundryCostDict = laundryCostData
.Where(x => x.StoreId != null)
.ToDictionary(x => x.StoreId.ToString(), x => Convert.ToDecimal(x.LaundryAmount ?? 0));
// 1.11 考勤数据 (lq_attendance_summary)
var attendanceList = await _db.Queryable()
.Where(x => x.Year == year && x.Month == month && x.IsEffective == 1)
.ToListAsync();
var attendanceDict = attendanceList.ToDictionary(x => x.UserId, x => x);
// 1.12 获取员工信息 (BASE_USER)
var userList = await _db.Queryable()
.Where(x => allManagerIds.Contains(x.Id))
.Select(x => new { x.Id, x.RealName, x.Account, x.IsOnJob })
.ToListAsync();
var userDict = userList.ToDictionary(x => x.Id, x => x);
// 2. 按总经理/经理聚合数据
var managerStats = new Dictionary();
foreach (var managerId in allManagerIds)
{
if (string.IsNullOrEmpty(managerId))
{
continue;
}
// 获取该总经理/经理的信息
var managerLifeline = lifelineList.FirstOrDefault(x => x.GeneralManagerId == managerId);
if (managerLifeline == null)
{
continue;
}
// 2.1 创建工资统计对象
var salary = new LqBusinessUnitManagerSalaryStatisticsEntity
{
Id = YitIdHelper.NextId().ToString(),
StatisticsMonth = monthStr,
EmployeeId = managerId,
ManagerType = managerLifeline.ManagerType,
Position = managerLifeline.ManagerType == 1 ? "总经理" : "经理",
IsTerminated = 0,
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
IsLocked = 0
};
// 2.2 填充员工信息
if (userDict.ContainsKey(managerId))
{
var user = userDict[managerId];
salary.EmployeeName = user.RealName ?? "";
salary.EmployeeAccount = user.Account ?? "";
salary.IsTerminated = user.IsOnJob == 0 ? 1 : 0;
}
// 2.3 考勤数据
var attendance = attendanceDict.ContainsKey(managerId) ? attendanceDict[managerId] : null;
salary.WorkingDays = attendance?.WorkDays ?? 0;
salary.LeaveDays = attendance?.LeaveDays ?? 0;
// 2.4 计算底薪(固定4000元)
salary.BaseSalary = 4000m;
// 2.5 遍历该总经理/经理管理的每个门店,计算提成
var storePerformanceDetails = new List();
decimal totalCommission = 0m;
decimal totalSalesPerformance = 0m;
decimal totalProductMaterial = 0m;
decimal totalCooperationCost = 0m;
decimal totalStoreExpense = 0m;
decimal totalLaundryCost = 0m;
decimal totalGrossProfit = 0m;
// 获取该总经理/经理管理的门店列表(如果没有管理的门店,则为空列表)
var managedStores = managerStoreDict.ContainsKey(managerId) ? managerStoreDict[managerId] : new List();
foreach (var storeId in managedStores)
{
if (string.IsNullOrEmpty(storeId))
{
continue;
}
// 获取该门店的提成阶梯设置
var storeLifelineSetting = lifelineList.FirstOrDefault(x => x.StoreId == storeId && x.GeneralManagerId == managerId);
if (storeLifelineSetting == null)
{
continue;
}
// 获取门店信息
var storeName = storeDict.ContainsKey(storeId) ? storeDict[storeId].Dm ?? "" : "";
// 获取门店生命线(仅用于记录)
var storeLifeline = storeLifelineDict.ContainsKey(storeId) ? storeLifelineDict[storeId] : 0;
// 计算销售业绩(开单业绩-退款业绩)
var billing = storeBillingDict.ContainsKey(storeId) ? storeBillingDict[storeId] : 0;
var refund = storeRefundDict.ContainsKey(storeId) ? storeRefundDict[storeId] : 0;
var salesPerformance = billing - refund;
// 统计各项成本
var productMaterial = productMaterialDict.ContainsKey(storeId) ? productMaterialDict[storeId] : 0;
var cooperationCost = cooperationCostDict.ContainsKey(storeId) ? cooperationCostDict[storeId] : 0;
var storeExpense = storeExpenseDict.ContainsKey(storeId) ? storeExpenseDict[storeId] : 0;
var laundryCost = laundryCostDict.ContainsKey(storeId) ? laundryCostDict[storeId] : 0;
// 计算毛利
// 毛利 = 销售业绩 - 产品物料 - 合作项目成本 - 店内支出 - 洗毛巾
var grossProfit = salesPerformance - productMaterial - cooperationCost - storeExpense - laundryCost;
// 累加各项数据
totalSalesPerformance += salesPerformance;
totalProductMaterial += productMaterial;
totalCooperationCost += cooperationCost;
totalStoreExpense += storeExpense;
totalLaundryCost += laundryCost;
totalGrossProfit += grossProfit;
// 计算提成(必须满足提成阶梯1才能有提成资格,使用毛利计算)
var commissionResult = CalculateStoreCommission(grossProfit, storeLifelineSetting);
var commissionAmount = commissionResult.Item1;
var calculationDetail = commissionResult.Item2;
totalCommission += commissionAmount;
// 添加到门店业绩明细
storePerformanceDetails.Add(new StorePerformanceDetail
{
StoreId = storeId,
StoreName = storeName,
StoreLifeline = storeLifeline,
BillingPerformance = billing,
RefundPerformance = refund,
SalesPerformance = salesPerformance,
ProductMaterial = productMaterial,
CooperationCost = cooperationCost,
StoreExpense = storeExpense,
LaundryCost = laundryCost,
GrossProfit = grossProfit,
StorePerformance = grossProfit, // 用于提成计算的业绩是毛利
ReachedLifeline1 = grossProfit >= storeLifelineSetting.Lifeline1, // 是否达到提成阶梯1
Lifeline1 = storeLifelineSetting.Lifeline1,
CommissionRate1 = storeLifelineSetting.CommissionRate1,
Lifeline2 = storeLifelineSetting.Lifeline2,
CommissionRate2 = storeLifelineSetting.CommissionRate2,
Lifeline3 = storeLifelineSetting.Lifeline3,
CommissionRate3 = storeLifelineSetting.CommissionRate3,
CommissionAmount = commissionAmount,
CalculationDetail = calculationDetail
});
}
// 2.6 保存门店业绩明细(JSON格式)
salary.StorePerformanceDetail = storePerformanceDetails.ToJson();
// 2.7 保存毛利相关数据
salary.SalesPerformance = totalSalesPerformance;
salary.ProductMaterial = totalProductMaterial;
salary.CooperationCost = totalCooperationCost;
salary.StoreExpense = totalStoreExpense;
salary.LaundryCost = totalLaundryCost;
salary.GrossProfit = totalGrossProfit;
// 2.8 提成合计
salary.TotalCommission = totalCommission;
// 2.9 计算应发工资
salary.CalculatedGrossSalary = salary.BaseSalary + salary.TotalCommission;
salary.FinalGrossSalary = salary.CalculatedGrossSalary;
// 2.9 初始化其他字段(默认值为0)
salary.MonthlyTrainingSubsidy = 0;
salary.MonthlyTransportSubsidy = 0;
salary.LastMonthTrainingSubsidy = 0;
salary.LastMonthTransportSubsidy = 0;
salary.TotalSubsidy = 0;
salary.MissingCard = 0;
salary.LateArrival = 0;
salary.LeaveDeduction = 0;
salary.SocialInsuranceDeduction = 0;
salary.RewardDeduction = 0;
salary.AccommodationDeduction = 0;
salary.StudyPeriodDeduction = 0;
salary.WorkClothesDeduction = 0;
salary.TotalDeduction = 0;
salary.Bonus = 0;
salary.ReturnPhoneDeposit = 0;
salary.ReturnAccommodationDeposit = 0;
salary.ActualSalary = salary.FinalGrossSalary - salary.TotalDeduction + salary.TotalSubsidy + salary.Bonus;
salary.MonthlyPaymentStatus = "未发放";
salary.PaidAmount = 0;
salary.PendingAmount = salary.ActualSalary;
salary.LastMonthSupplement = 0;
salary.MonthlyTotalPayment = 0;
managerStats[managerId] = salary;
}
// 3. 保存数据
if (managerStats.Any())
{
// 3.1 先删除计算月的未锁定且未确认的工资记录
var deletedCount = await _db.Deleteable()
.Where(x => x.StatisticsMonth == monthStr
&& x.IsLocked == 0
&& x.EmployeeConfirmStatus == 0)
.ExecuteCommandAsync();
if (deletedCount > 0)
{
_logger.LogInformation($"计算工资前删除了 {deletedCount} 条未锁定且未确认的记录(月份:{monthStr})");
}
// 3.2 查询已存在的记录(只查询已锁定或已确认的记录)
var existingRecords = await _db.Queryable()
.Where(x => x.StatisticsMonth == monthStr
&& (x.IsLocked == 1 || x.EmployeeConfirmStatus == 1))
.ToListAsync();
var existingDict = existingRecords.Where(x => !string.IsNullOrEmpty(x.EmployeeId))
.GroupBy(x => x.EmployeeId).ToDictionary(g => g.Key, g => g.First());
var recordsToInsert = new List();
var recordsToUpdate = new List();
var updatedCount = 0;
var skippedCount = 0;
foreach (var salary in managerStats.Values)
{
if (existingDict.ContainsKey(salary.EmployeeId))
{
// 检查记录是否已锁定或已确认
var existing = existingDict[salary.EmployeeId];
// 如果已锁定或已确认,跳过不更新(保留所有原有数据,包括扣款项目)
if (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1)
{
skippedCount++;
continue; // 跳过,不进行任何更新
}
// 未锁定且未确认的记录,可以做更新操作
salary.Id = existing.Id;
salary.EmployeeConfirmStatus = existing.EmployeeConfirmStatus; // 应该是0
salary.EmployeeConfirmTime = existing.EmployeeConfirmTime;
salary.EmployeeConfirmRemark = existing.EmployeeConfirmRemark;
salary.IsLocked = existing.IsLocked; // 保留锁定状态(应该是0)
salary.CreateTime = existing.CreateTime;
salary.CreateUser = existing.CreateUser;
recordsToUpdate.Add(salary);
updatedCount++;
}
else
{
// 不存在的记录,做插入操作
salary.Id = YitIdHelper.NextId().ToString();
salary.EmployeeConfirmStatus = 0;
salary.IsLocked = 0;
salary.CreateTime = DateTime.Now;
salary.CreateUser = "";
recordsToInsert.Add(salary);
}
}
if (recordsToInsert.Any())
{
await _db.Insertable(recordsToInsert).ExecuteCommandAsync();
_logger.LogInformation($"插入了 {recordsToInsert.Count} 条新的工资记录(月份:{monthStr})");
}
if (recordsToUpdate.Any())
{
await _db.Updateable(recordsToUpdate).ExecuteCommandAsync();
_logger.LogInformation($"更新了 {recordsToUpdate.Count} 条未锁定且未确认的工资记录(月份:{monthStr})");
}
if (skippedCount > 0)
{
_logger.LogInformation($"跳过了 {skippedCount} 条已锁定或已确认的工资记录,保留原有数据(月份:{monthStr})");
}
}
}
///
/// 计算门店提成(分段累进式)
///
/// 门店毛利
/// 提成阶梯设置
/// 提成金额和计算说明
///
/// 提成规则(分段累进式):
/// 1. 前提条件:必须满足提成阶梯1才能有提成资格
/// 2. 提成基数:使用毛利计算,而不是开单业绩
/// 3. 分段累进式计算:不同区间按不同比例分别计算后累加
/// - 毛利 < 提成阶梯1:无提成
/// - 提成阶梯1 ≤ 毛利 < 提成阶梯2:提成阶梯1 × 提成比例1 + (毛利 - 提成阶梯1) × 提成比例2
/// - 提成阶梯2 ≤ 毛利 < 提成阶梯3:提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (毛利 - 提成阶梯2) × 提成比例3
/// - 毛利 ≥ 提成阶梯3:提成阶梯1 × 提成比例1 + (提成阶梯2 - 提成阶梯1) × 提成比例2 + (提成阶梯3 - 提成阶梯2) × 提成比例3 + (毛利 - 提成阶梯3) × 提成比例3
///
/// 示例:毛利 = 100,000元,提成阶梯1 = 150,000元,提成比例1 = 1%
/// 计算:100,000 < 150,000,未达到提成阶梯1,提成 = 0元
///
private (decimal Amount, string Detail) CalculateStoreCommission(decimal grossProfit, LqMdGeneralManagerLifelineEntity lifelineSetting)
{
// 验证提成阶梯1和提成比例1必须设置
if (lifelineSetting.Lifeline1 <= 0 || lifelineSetting.CommissionRate1 <= 0)
{
return (0m, "提成阶梯1或提成比例1未设置,无法计算提成");
}
// 必须满足提成阶梯1才能有提成资格
if (grossProfit < lifelineSetting.Lifeline1)
{
return (0m, $"毛利{grossProfit:N2}元,< 提成阶梯1({lifelineSetting.Lifeline1:N2}元),未达到提成资格,提成 = 0元");
}
decimal commissionAmount = 0m;
string detail = "";
var lifeline1 = lifelineSetting.Lifeline1;
var rate1 = lifelineSetting.CommissionRate1;
var lifeline2 = lifelineSetting.Lifeline2 ?? 0;
var rate2 = lifelineSetting.CommissionRate2 ?? 0;
var lifeline3 = lifelineSetting.Lifeline3 ?? 0;
var rate3 = lifelineSetting.CommissionRate3 ?? 0;
// 分段累进式计算(已通过提成阶梯1检查)
if (lifeline2 > 0 && grossProfit < lifeline2)
{
// 提成阶梯1 ≤ 毛利 < 提成阶梯2:分段累进计算
var part1 = lifeline1 * (rate1 / 100m);
var part2 = (grossProfit - lifeline1) * (rate2 / 100m);
commissionAmount = part1 + part2;
detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯1({lifeline1:N2}元) 且 < 提成阶梯2({lifeline2:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
}
else if (lifeline3 > 0 && grossProfit < lifeline3)
{
// 提成阶梯2 ≤ 毛利 < 提成阶梯3:分段累进计算
var part1 = lifeline1 * (rate1 / 100m);
var part2 = (lifeline2 - lifeline1) * (rate2 / 100m);
var part3 = (grossProfit - lifeline2) * (rate3 / 100m);
commissionAmount = part1 + part2 + part3;
detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯2({lifeline2:N2}元) 且 < 提成阶梯3({lifeline3:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({lifeline2:N2} - {lifeline1:N2}) × {rate2}% + ({grossProfit:N2} - {lifeline2:N2}) × {rate3}% = {part1:N2} + {part2:N2} + {part3:N2} = {commissionAmount:N2}元";
}
else if (lifeline3 > 0)
{
// 毛利 ≥ 提成阶梯3:分段累进计算
var part1 = lifeline1 * (rate1 / 100m);
var part2 = (lifeline2 - lifeline1) * (rate2 / 100m);
var part3 = (lifeline3 - lifeline2) * (rate3 / 100m);
var part4 = (grossProfit - lifeline3) * (rate3 / 100m);
commissionAmount = part1 + part2 + part3 + part4;
detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯3({lifeline3:N2}元),提成 = {lifeline1:N2} × {rate1}% + ({lifeline2:N2} - {lifeline1:N2}) × {rate2}% + ({lifeline3:N2} - {lifeline2:N2}) × {rate3}% + ({grossProfit:N2} - {lifeline3:N2}) × {rate3}% = {part1:N2} + {part2:N2} + {part3:N2} + {part4:N2} = {commissionAmount:N2}元";
}
else if (lifeline2 > 0)
{
// 提成阶梯3未设置,毛利 ≥ 提成阶梯2:分段累进计算
var part1 = lifeline1 * (rate1 / 100m);
var part2 = (grossProfit - lifeline1) * (rate2 / 100m);
commissionAmount = part1 + part2;
detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯2({lifeline2:N2}元),提成阶梯3未设置,提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate2}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
}
else
{
// 只有提成阶梯1,毛利 ≥ 提成阶梯1:分段累进计算(提成阶梯1部分 × 提成比例1 + 超出部分 × 提成比例1)
var part1 = lifeline1 * (rate1 / 100m);
var part2 = (grossProfit - lifeline1) * (rate1 / 100m);
commissionAmount = part1 + part2;
detail = $"毛利{grossProfit:N2}元,≥ 提成阶梯1({lifeline1:N2}元),提成阶梯2未设置,提成 = {lifeline1:N2} × {rate1}% + ({grossProfit:N2} - {lifeline1:N2}) × {rate1}% = {part1:N2} + {part2:N2} = {commissionAmount:N2}元";
}
return (commissionAmount, detail);
}
///
/// 门店业绩明细(用于JSON序列化)
///
private class StorePerformanceDetail
{
public string StoreId { get; set; }
public string StoreName { get; set; }
public decimal StoreLifeline { get; set; }
public decimal BillingPerformance { get; set; }
public decimal RefundPerformance { get; set; }
public decimal SalesPerformance { get; set; }
public decimal ProductMaterial { get; set; }
public decimal CooperationCost { get; set; }
public decimal StoreExpense { get; set; }
public decimal LaundryCost { get; set; }
public decimal GrossProfit { get; set; }
public decimal StorePerformance { get; set; } // 用于提成计算的业绩(等于毛利)
public bool ReachedLifeline1 { get; set; } // 是否达到提成阶梯1
public decimal Lifeline1 { get; set; }
public decimal CommissionRate1 { get; set; }
public decimal? Lifeline2 { get; set; }
public decimal? CommissionRate2 { get; set; }
public decimal? Lifeline3 { get; set; }
public decimal? CommissionRate3 { get; set; }
public decimal CommissionAmount { get; set; }
public string CalculationDetail { get; set; }
}
#region 员工工资确认
///
/// 员工确认工资条
///
///
/// 员工确认自己的工资条,确认后工资数据不可再修改
///
/// 示例请求:
///
/// {
/// "id": "工资记录ID",
/// "employeeId": "员工ID",
/// "remark": "确认备注(可选)"
/// }
///
///
/// 参数说明:
/// - id: 工资记录ID(必填)
/// - employeeId: 员工ID(必填)
/// - remark: 确认备注(可选)
///
/// 注意事项:
/// - 只能确认自己的工资条
/// - 只能确认已锁定的工资条(IsLocked = 1)
/// - 已确认的工资条不能重复确认
///
/// 确认参数
/// 操作结果
/// 确认成功
/// 参数错误或记录不存在
[HttpPost("confirm")]
public async Task ConfirmSalary([FromBody] SalaryConfirmInput input)
{
try
{
if (string.IsNullOrWhiteSpace(input.Id) || string.IsNullOrWhiteSpace(input.EmployeeId))
throw NCCException.Oh("工资记录ID和员工ID不能为空");
var salary = await _db.Queryable()
.Where(s => s.Id == input.Id && s.EmployeeId == input.EmployeeId).FirstAsync();
if (salary == null) throw NCCException.Oh("工资记录不存在或不属于该员工");
if (salary.EmployeeConfirmStatus == 1) throw NCCException.Oh("该工资条已确认,不能重复确认");
if (salary.IsLocked != 1) throw NCCException.Oh("该工资条尚未锁定,请等待管理员锁定后再确认");
salary.EmployeeConfirmStatus = 1;
salary.EmployeeConfirmTime = DateTime.Now;
salary.EmployeeConfirmRemark = input.Remark;
salary.UpdateTime = DateTime.Now;
await _db.Updateable(salary).ExecuteCommandAsync();
return "确认成功";
}
catch (Exception ex)
{
throw NCCException.Oh($"确认工资条失败: {ex.Message}");
}
}
#endregion
#region 工资锁定/解锁
///
/// 批量锁定/解锁工资条
///
[HttpPost("lock")]
public async Task LockSalary([FromBody] SalaryLockInput input)
{
try
{
if (input == null || input.Ids == null || !input.Ids.Any())
throw NCCException.Oh("工资记录ID列表不能为空");
var salaries = await _db.Queryable()
.Where(s => input.Ids.Contains(s.Id))
.ToListAsync();
if (!salaries.Any())
throw NCCException.Oh("未找到指定的工资记录");
var lockedCount = 0;
var unlockedCount = 0;
var skippedCount = 0;
foreach (var salary in salaries)
{
if (salary.EmployeeConfirmStatus == 1 && !input.IsLocked)
{
skippedCount++;
continue;
}
salary.IsLocked = input.IsLocked ? 1 : 0;
salary.UpdateTime = DateTime.Now;
if (input.IsLocked) lockedCount++; else unlockedCount++;
}
await _db.Updateable(salaries).ExecuteCommandAsync();
var action = input.IsLocked ? "锁定" : "解锁";
var count = input.IsLocked ? lockedCount : unlockedCount;
var message = $"{action}成功:{count}条";
if (skippedCount > 0)
message += $",跳过{skippedCount}条(已确认的记录不能解锁)";
return message;
}
catch (Exception ex)
{
throw NCCException.Oh($"锁定/解锁工资条失败: {ex.Message}");
}
}
///
/// 批量锁定当月所有工资
///
/// 批量锁定输入参数
/// 锁定结果
[HttpPost("lock-by-month")]
public async Task LockSalaryByMonth([FromBody] SalaryLockByMonthInput input)
{
try
{
if (input == null)
throw NCCException.Oh("参数不能为空");
if (input.Year <= 0 || input.Month <= 0 || input.Month > 12)
throw NCCException.Oh("年份和月份参数不正确");
var monthStr = $"{input.Year}{input.Month:D2}";
var salaries = await _db.Queryable()
.Where(s => s.StatisticsMonth == monthStr)
.ToListAsync();
if (!salaries.Any())
throw NCCException.Oh($"未找到{input.Year}年{input.Month}月的工资记录");
var lockedCount = 0;
var unlockedCount = 0;
var skippedCount = 0;
var alreadyLockedCount = 0;
foreach (var salary in salaries)
{
if (salary.EmployeeConfirmStatus == 1 && !input.IsLocked)
{
skippedCount++;
continue;
}
if (salary.IsLocked == 1 && input.IsLocked)
{
alreadyLockedCount++;
continue;
}
if (salary.IsLocked == 0 && !input.IsLocked)
{
alreadyLockedCount++;
continue;
}
salary.IsLocked = input.IsLocked ? 1 : 0;
salary.UpdateTime = DateTime.Now;
if (input.IsLocked)
lockedCount++;
else
unlockedCount++;
}
if (lockedCount > 0 || unlockedCount > 0)
{
var salariesToUpdate = salaries.Where(s =>
(input.IsLocked && s.IsLocked == 0) ||
(!input.IsLocked && s.IsLocked == 1 && s.EmployeeConfirmStatus != 1)
).ToList();
if (salariesToUpdate.Any())
{
await _db.Updateable(salariesToUpdate)
.UpdateColumns(s => new { s.IsLocked, s.UpdateTime })
.ExecuteCommandAsync();
}
}
var action = input.IsLocked ? "锁定" : "解锁";
var count = input.IsLocked ? lockedCount : unlockedCount;
var message = $"{action}成功:{count}条";
if (alreadyLockedCount > 0)
message += $",跳过{alreadyLockedCount}条(已是{action}状态)";
if (skippedCount > 0)
message += $",跳过{skippedCount}条(已确认的记录不能解锁)";
return new
{
success = true,
message = message,
total = salaries.Count,
locked = lockedCount,
unlocked = unlockedCount,
skipped = skippedCount,
alreadyLocked = alreadyLockedCount
};
}
catch (Exception ex)
{
_logger.LogError(ex, "批量锁定当月工资失败");
var action = input?.IsLocked == true ? "锁定" : "解锁";
throw NCCException.Oh($"批量{action}当月工资失败: {ex.Message}");
}
}
#endregion
#region 导入工资
///
/// 从Excel导入事业部总经理工资数据
///
/// Excel文件
/// 导入结果
[HttpPost("import")]
public async Task ImportSalaryFromExcel(IFormFile file)
{
try
{
if (file == null || file.Length == 0)
throw NCCException.Oh("请选择要上传的Excel文件");
var allowedExtensions = new[] { ".xlsx", ".xls" };
var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(fileExtension))
throw NCCException.Oh("只支持.xlsx和.xls格式的Excel文件");
var recordsToInsert = new List();
var recordsToUpdate = new List();
var errorMessages = new List();
var successCount = 0;
var failCount = 0;
var skippedCount = 0;
var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName));
try
{
using (var stream = new FileStream(tempFilePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
var dataTable = ExcelImportHelper.ToDataTable(tempFilePath, 0, 0);
if (dataTable.Rows.Count == 0)
throw NCCException.Oh("Excel文件中没有数据行");
Func ParseDecimal = (str) =>
{
if (string.IsNullOrWhiteSpace(str)) return 0;
var cleaned = str.Trim().Replace(",", "").Replace(",", "").Replace("¥", "").Replace("$", "").Replace("元", "").Replace("%", "").Replace(" ", "");
return decimal.TryParse(cleaned, out decimal result) ? result : 0;
};
Func ParseInt = (str) =>
{
if (string.IsNullOrWhiteSpace(str)) return 0;
var cleaned = str.Trim().Replace(",", "").Replace(",", "").Replace(" ", "");
return int.TryParse(cleaned, out int result) ? result : 0;
};
// ExcelImportHelper.ToDataTable(filePath, 0, 0)会将第一行作为标题行,数据从第二行开始
// 所以DataTable.Rows[0]是Excel的第一条数据行,应该从i=0开始循环
for (int i = 0; i < dataTable.Rows.Count; i++)
{
try
{
var row = dataTable.Rows[i];
Func GetColumnValue = (colIndex) => colIndex < row.ItemArray.Length && row[colIndex] != null ? row[colIndex].ToString().Trim() : "";
var firstColumnValue = GetColumnValue(0);
bool isOldFormat = !string.IsNullOrWhiteSpace(firstColumnValue) && (firstColumnValue == "员工姓名" || (!long.TryParse(firstColumnValue, out _) && firstColumnValue.Length > 20));
int employeeNameIndex = isOldFormat ? 0 : 1;
int offset = isOldFormat ? 0 : 1;
var id = isOldFormat ? "" : GetColumnValue(0);
var employeeName = GetColumnValue(employeeNameIndex);
if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(employeeName))
continue;
if (string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(employeeName))
{
var matchedRecord = await _db.Queryable()
.Where(x => x.EmployeeName == employeeName)
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.FirstAsync();
if (matchedRecord != null) id = matchedRecord.Id;
}
if (string.IsNullOrWhiteSpace(employeeName))
{
errorMessages.Add($"第{i + 1}行:员工姓名不能为空");
failCount++;
continue;
}
LqBusinessUnitManagerSalaryStatisticsEntity existing = null;
if (!string.IsNullOrWhiteSpace(id))
{
existing = await _db.Queryable()
.Where(x => x.Id == id).FirstAsync();
if (existing != null && (existing.IsLocked == 1 || existing.EmployeeConfirmStatus == 1))
{
skippedCount++;
failCount++;
continue;
}
}
var entity = existing ?? new LqBusinessUnitManagerSalaryStatisticsEntity
{
Id = string.IsNullOrWhiteSpace(id) ? YitIdHelper.NextId().ToString() : id,
EmployeeConfirmStatus = 0,
IsLocked = 0,
CreateTime = DateTime.Now,
CreateUser = ""
};
// Excel字段映射(事业部总经理工资35列:员工姓名,员工账号,核算岗位,经理类型,统计月份,底薪,提成合计,在店天数,请假天数,核算应发工资...)
entity.EmployeeName = employeeName;
entity.EmployeeAccount = GetColumnValue(1 + offset);
entity.Position = GetColumnValue(2 + offset);
var managerTypeStr = GetColumnValue(3 + offset);
entity.ManagerType = managerTypeStr == "总经理" || managerTypeStr == "1" ? 1 : 0;
entity.StatisticsMonth = GetColumnValue(4 + offset);
entity.BaseSalary = ParseDecimal(GetColumnValue(5 + offset));
entity.TotalCommission = ParseDecimal(GetColumnValue(6 + offset));
entity.WorkingDays = ParseDecimal(GetColumnValue(7 + offset));
entity.LeaveDays = ParseDecimal(GetColumnValue(8 + offset));
entity.CalculatedGrossSalary = ParseDecimal(GetColumnValue(9 + offset));
entity.FinalGrossSalary = ParseDecimal(GetColumnValue(10 + offset));
entity.MonthlyTrainingSubsidy = ParseDecimal(GetColumnValue(11 + offset));
entity.MonthlyTransportSubsidy = ParseDecimal(GetColumnValue(12 + offset));
entity.LastMonthTrainingSubsidy = ParseDecimal(GetColumnValue(13 + offset));
entity.LastMonthTransportSubsidy = ParseDecimal(GetColumnValue(14 + offset));
entity.TotalSubsidy = ParseDecimal(GetColumnValue(15 + offset));
entity.MissingCard = ParseDecimal(GetColumnValue(16 + offset));
entity.LateArrival = ParseDecimal(GetColumnValue(17 + offset));
entity.LeaveDeduction = ParseDecimal(GetColumnValue(18 + offset));
entity.SocialInsuranceDeduction = ParseDecimal(GetColumnValue(19 + offset));
entity.RewardDeduction = ParseDecimal(GetColumnValue(20 + offset));
entity.AccommodationDeduction = ParseDecimal(GetColumnValue(21 + offset));
entity.StudyPeriodDeduction = ParseDecimal(GetColumnValue(22 + offset));
entity.WorkClothesDeduction = ParseDecimal(GetColumnValue(23 + offset));
entity.TotalDeduction = ParseDecimal(GetColumnValue(24 + offset));
entity.Bonus = ParseDecimal(GetColumnValue(25 + offset));
entity.ReturnPhoneDeposit = ParseDecimal(GetColumnValue(26 + offset));
entity.ReturnAccommodationDeposit = ParseDecimal(GetColumnValue(27 + offset));
entity.LastMonthSupplement = ParseDecimal(GetColumnValue(28 + offset));
entity.ActualSalary = ParseDecimal(GetColumnValue(29 + offset));
entity.MonthlyPaymentStatus = GetColumnValue(30 + offset);
entity.PaidAmount = ParseDecimal(GetColumnValue(31 + offset));
entity.PendingAmount = ParseDecimal(GetColumnValue(32 + offset));
entity.MonthlyTotalPayment = ParseDecimal(GetColumnValue(33 + offset));
var isTerminatedStr = GetColumnValue(34 + offset);
entity.IsTerminated = isTerminatedStr == "离职" || isTerminatedStr == "1" ? 1 : 0;
if (existing != null)
{
entity.EmployeeId = existing.EmployeeId;
entity.StorePerformanceDetail = existing.StorePerformanceDetail;
}
else
{
if (!string.IsNullOrWhiteSpace(employeeName))
{
var user = await _db.Queryable()
.Where(u => u.RealName == employeeName).FirstAsync();
if (user != null) entity.EmployeeId = user.Id;
}
}
entity.UpdateTime = DateTime.Now;
if (existing != null) recordsToUpdate.Add(entity);
else recordsToInsert.Add(entity);
successCount++;
}
catch (Exception ex)
{
errorMessages.Add($"第{i + 1}行数据处理失败: {ex.Message}");
failCount++;
}
}
}
finally
{
if (File.Exists(tempFilePath)) File.Delete(tempFilePath);
}
if (recordsToInsert.Any()) await _db.Insertable(recordsToInsert).ExecuteCommandAsync();
if (recordsToUpdate.Any())
{
// 使用IgnoreColumns排除CreateTime和CreateUser,确保其他所有字段都被更新
await _db.Updateable(recordsToUpdate)
.IgnoreColumns(x => x.CreateTime)
.IgnoreColumns(x => x.CreateUser)
.ExecuteCommandAsync();
}
return new
{
success = true,
message = $"导入完成:成功 {successCount} 条,失败 {failCount} 条,跳过 {skippedCount} 条(已锁定或已确认)",
successCount,
failCount,
skippedCount,
errors = errorMessages
};
}
catch (Exception ex)
{
throw NCCException.Oh($"导入事业部总经理工资数据失败: {ex.Message}");
}
}
#endregion
}
}