using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NCC.Common.Core.Manager; using NCC.Common.Filter; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqContract; using NCC.Extend.Entitys.Enum; using NCC.Extend.Entitys.lq_contract; using NCC.Extend.Entitys.lq_contract_rent_detail; using NCC.Extend.Entitys.lq_contract_monthly_cost; using NCC.Extend.Entitys.lq_mdxx; using NCC.FriendlyException; using NCC.System.Entitys.Permission; using SqlSugar; using Yitter.IdGenerator; namespace NCC.Extend { /// /// 合同管理服务 /// [ApiDescriptionSettings(Tag = "绿纤合同管理", Name = "LqContract", Order = 250)] [Route("api/Extend/LqContract")] public class LqContractService : IDynamicApiController, ITransient { private readonly IUserManager _userManager; private readonly ILogger _logger; private readonly ISqlSugarClient _db; /// /// 构造函数 /// /// 用户管理器 /// 日志记录器 /// 数据库客户端 public LqContractService(IUserManager userManager, ILogger logger, ISqlSugarClient db) { _userManager = userManager; _logger = logger; _db = db; } #region 创建合同 /// /// 创建合同 /// /// /// 创建新合同,系统会自动根据合同信息生成月租明细 /// /// 示例请求: /// ```json /// { /// "storeId": "门店ID", /// "title": "门店租赁合同", /// "category": "租赁合同", /// "tenantName": "张三", /// "contractStartDate": "2025-01-01T00:00:00", /// "contractEndDate": "2025-12-31T23:59:59", /// "reminderDays": 7, /// "deposit": 5000.00, /// "monthlyRent": 1000.00, /// "paymentAmount": 3000.00, /// "paymentCycle": 3, /// "remarks": "季度交租", /// "attachment": "" /// } /// ``` /// /// 业务流程: /// 1. 验证合同信息 /// 2. 查询门店信息(获取店名) /// 3. 创建合同记录 /// 4. 自动生成月租明细(根据合同起始日期、结束日期、交租周期、缴租金额) /// 5. 计算下次应交时间 /// /// 创建输入 /// 创建结果 /// 创建成功 /// 请求参数错误 /// 服务器错误 [HttpPost("Create")] public async Task CreateAsync([FromBody] LqContractCrInput input) { try { // 验证合同日期 if (input.ContractStartDate >= input.ContractEndDate) { throw NCCException.Oh("合同起始日期必须小于合同结束日期"); } // 验证交租周期 if (input.PaymentCycle <= 0 || input.PaymentCycle > 12) { throw NCCException.Oh("交租周期必须在1-12个月之间"); } // 查询门店信息 var store = await _db.Queryable() .Where(x => x.Id == input.StoreId) .Select(x => new { x.Dm }) .FirstAsync(); if (store == null) { throw NCCException.Oh("门店不存在"); } _db.Ado.BeginTran(); try { // 创建合同 var contractEntity = new LqContractEntity { Id = YitIdHelper.NextId().ToString(), StoreId = input.StoreId, StoreName = store.Dm ?? "", Title = input.Title, Category = input.Category, TenantName = input.TenantName, ContractStartDate = input.ContractStartDate, ContractEndDate = input.ContractEndDate, ReminderDays = input.ReminderDays, Deposit = input.Deposit, MonthlyRent = input.MonthlyRent, PaymentAmount = input.PaymentAmount, PaymentCycle = input.PaymentCycle, Remarks = input.Remarks, Attachment = input.Attachment, CreateUser = _userManager.UserId, CreateTime = DateTime.Now, IsEffective = StatusEnum.有效.GetHashCode() }; var insertCount = await _db.Insertable(contractEntity).ExecuteCommandAsync(); if (insertCount <= 0) { throw NCCException.Oh("创建合同失败"); } // 自动生成月租明细 await GenerateRentDetailsAsync(contractEntity.Id, contractEntity.ContractStartDate, contractEntity.ContractEndDate, contractEntity.PaymentCycle, contractEntity.PaymentAmount); // 自动生成按月成本记录 await GenerateMonthlyCostAsync(contractEntity.Id, contractEntity.StoreId, contractEntity.StoreName, contractEntity.Category, contractEntity.ContractStartDate, contractEntity.ContractEndDate, contractEntity.PaymentCycle, contractEntity.PaymentAmount); // 计算下次应交时间 await CalculateNextPaymentDate(contractEntity.Id); _db.Ado.CommitTran(); } catch { _db.Ado.RollbackTran(); throw; } } catch (Exception ex) { _db.Ado.RollbackTran(); _logger.LogError(ex, "创建合同失败"); throw NCCException.Oh($"创建失败:{ex.Message}"); } } #endregion #region 更新合同 /// /// 更新合同 /// /// /// 更新合同信息。如果修改了合同起始日期、结束日期、交租周期或缴租金额,系统会重新生成月租明细。 /// /// 示例请求: /// ```json /// { /// "id": "合同ID", /// "storeId": "门店ID", /// "title": "门店租赁合同", /// "category": "租赁合同", /// "tenantName": "张三", /// "contractStartDate": "2025-01-01T00:00:00", /// "contractEndDate": "2025-12-31T23:59:59", /// "reminderDays": 7, /// "deposit": 5000.00, /// "monthlyRent": 1000.00, /// "paymentAmount": 3000.00, /// "paymentCycle": 3, /// "remarks": "季度交租", /// "attachment": "" /// } /// ``` /// /// 更新输入 /// 更新结果 /// 更新成功 /// 请求参数错误或合同不存在 /// 服务器错误 [HttpPut("Update")] public async Task UpdateAsync([FromBody] LqContractUpInput input) { try { // 查询合同 var contract = await _db.Queryable() .Where(x => x.Id == input.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) .FirstAsync(); if (contract == null) { throw NCCException.Oh("合同不存在或已失效"); } // 验证合同日期 if (input.ContractStartDate >= input.ContractEndDate) { throw NCCException.Oh("合同起始日期必须小于合同结束日期"); } // 验证交租周期 if (input.PaymentCycle <= 0 || input.PaymentCycle > 12) { throw NCCException.Oh("交租周期必须在1-12个月之间"); } // 查询门店信息 var store = await _db.Queryable() .Where(x => x.Id == input.StoreId) .Select(x => new { x.Dm }) .FirstAsync(); if (store == null) { throw NCCException.Oh("门店不存在"); } _db.Ado.BeginTran(); try { // 判断是否需要重新生成明细 bool needRegenerateDetails = contract.ContractStartDate != input.ContractStartDate || contract.ContractEndDate != input.ContractEndDate || contract.PaymentCycle != input.PaymentCycle || contract.PaymentAmount != input.PaymentAmount; // 判断是否需要更新成本记录的分类(如果只修改了分类,不需要重新生成,只需更新分类字段) bool needUpdateCategory = contract.Category != input.Category; // 更新合同信息 contract.StoreId = input.StoreId; contract.StoreName = store.Dm ?? ""; contract.Title = input.Title; contract.Category = input.Category; contract.TenantName = input.TenantName; contract.ContractStartDate = input.ContractStartDate; contract.ContractEndDate = input.ContractEndDate; contract.ReminderDays = input.ReminderDays; contract.Deposit = input.Deposit; contract.MonthlyRent = input.MonthlyRent; contract.PaymentAmount = input.PaymentAmount; contract.PaymentCycle = input.PaymentCycle; contract.Remarks = input.Remarks; contract.Attachment = input.Attachment; contract.UpdateUser = _userManager.UserId; contract.UpdateTime = DateTime.Now; var updateCount = await _db.Updateable(contract).ExecuteCommandAsync(); if (updateCount <= 0) { throw NCCException.Oh("更新合同失败"); } // 如果需要重新生成明细,先删除旧明细,再生成新明细 if (needRegenerateDetails) { // 删除旧的明细(逻辑删除) await _db.Updateable() .SetColumns(x => new LqContractRentDetailEntity { IsEffective = StatusEnum.无效.GetHashCode(), UpdateUser = _userManager.UserId, UpdateTime = DateTime.Now }) .Where(x => x.ContractId == contract.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) .ExecuteCommandAsync(); // 删除旧的成本记录(逻辑删除) await _db.Updateable() .SetColumns(x => new LqContractMonthlyCostEntity { IsEffective = StatusEnum.无效.GetHashCode(), UpdateTime = DateTime.Now }) .Where(x => x.ContractId == contract.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) .ExecuteCommandAsync(); // 生成新的明细 await GenerateRentDetailsAsync(contract.Id, contract.ContractStartDate, contract.ContractEndDate, contract.PaymentCycle, contract.PaymentAmount); // 生成新的成本记录 await GenerateMonthlyCostAsync(contract.Id, contract.StoreId, contract.StoreName, contract.Category, contract.ContractStartDate, contract.ContractEndDate, contract.PaymentCycle, contract.PaymentAmount); } else if (needUpdateCategory) { // 如果只修改了分类,只需更新成本记录的分类字段 await _db.Updateable() .SetColumns(x => new LqContractMonthlyCostEntity { Category = contract.Category, UpdateTime = DateTime.Now }) .Where(x => x.ContractId == contract.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) .ExecuteCommandAsync(); } // 重新计算下次应交时间 await CalculateNextPaymentDate(contract.Id); _db.Ado.CommitTran(); } catch { _db.Ado.RollbackTran(); throw; } } catch (Exception ex) { _db.Ado.RollbackTran(); _logger.LogError(ex, "更新合同失败"); throw NCCException.Oh($"更新失败:{ex.Message}"); } } #endregion #region 删除合同 /// /// 删除合同 /// /// /// 删除合同,同时会级联删除该合同的所有月租明细(逻辑删除) /// /// 示例请求: /// ``` /// DELETE /api/Extend/LqContract/{id} /// ``` /// /// 合同ID /// 删除结果 /// 删除成功 /// 合同不存在 /// 服务器错误 [HttpDelete("{id}")] public async Task DeleteAsync([FromRoute] string id) { try { if (string.IsNullOrWhiteSpace(id)) { throw NCCException.Oh("合同ID不能为空"); } // 查询合同 var contract = await _db.Queryable() .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode()) .FirstAsync(); if (contract == null) { throw NCCException.Oh("合同不存在或已失效"); } _db.Ado.BeginTran(); try { // 删除该合同的所有月租明细(逻辑删除) await _db.Updateable() .SetColumns(x => new LqContractRentDetailEntity { IsEffective = StatusEnum.无效.GetHashCode(), UpdateUser = _userManager.UserId, UpdateTime = DateTime.Now }) .Where(x => x.ContractId == id && x.IsEffective == StatusEnum.有效.GetHashCode()) .ExecuteCommandAsync(); // 删除该合同的所有成本记录(逻辑删除) await _db.Updateable() .SetColumns(x => new LqContractMonthlyCostEntity { IsEffective = StatusEnum.无效.GetHashCode(), UpdateTime = DateTime.Now }) .Where(x => x.ContractId == id && x.IsEffective == StatusEnum.有效.GetHashCode()) .ExecuteCommandAsync(); // 删除合同(逻辑删除) contract.IsEffective = StatusEnum.无效.GetHashCode(); contract.UpdateUser = _userManager.UserId; contract.UpdateTime = DateTime.Now; var updateCount = await _db.Updateable(contract).ExecuteCommandAsync(); if (updateCount <= 0) { throw NCCException.Oh("删除合同失败"); } _db.Ado.CommitTran(); } catch { _db.Ado.RollbackTran(); throw; } } catch (Exception ex) { _db.Ado.RollbackTran(); _logger.LogError(ex, "删除合同失败"); throw NCCException.Oh($"删除失败:{ex.Message}"); } } #endregion #region 获取合同列表 /// /// 获取合同列表 /// /// /// 分页查询合同列表,支持多条件筛选 /// /// 示例请求: /// ``` /// GET /api/Extend/LqContract/GetList?currentPage=1&pageSize=10&storeId=门店ID&title=合同标题 /// ``` /// /// 查询输入 /// 合同列表 /// 查询成功 /// 服务器错误 [HttpGet("GetList")] public async Task GetListAsync([FromQuery] LqContractListQueryInput input) { try { var sidx = string.IsNullOrEmpty(input.sidx) ? "createTime" : input.sidx; var sort = string.IsNullOrEmpty(input.sort) ? "desc" : input.sort; var data = await _db.Queryable() .WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), x => x.StoreId == input.StoreId) .WhereIF(!string.IsNullOrWhiteSpace(input.StoreName), x => x.StoreName != null && x.StoreName.Contains(input.StoreName)) .WhereIF(!string.IsNullOrWhiteSpace(input.Category), x => x.Category == input.Category) .WhereIF(!string.IsNullOrWhiteSpace(input.Title), x => x.Title != null && x.Title.Contains(input.Title)) .WhereIF(input.ContractStartDateBegin.HasValue, x => x.ContractStartDate >= input.ContractStartDateBegin.Value) .WhereIF(input.ContractStartDateEnd.HasValue, x => x.ContractStartDate <= input.ContractStartDateEnd.Value) .WhereIF(input.ContractEndDateBegin.HasValue, x => x.ContractEndDate >= input.ContractEndDateBegin.Value) .WhereIF(input.ContractEndDateEnd.HasValue, x => x.ContractEndDate <= input.ContractEndDateEnd.Value) .WhereIF(input.IsEffective.HasValue, x => x.IsEffective == input.IsEffective.Value) .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) .Select(x => new LqContractListOutput { id = x.Id, storeId = x.StoreId, storeName = x.StoreName, title = x.Title, category = x.Category, tenantName = x.TenantName, contractStartDate = x.ContractStartDate, contractEndDate = x.ContractEndDate, reminderDays = x.ReminderDays, deposit = x.Deposit, nextPaymentDate = x.NextPaymentDate, monthlyRent = x.MonthlyRent, paymentAmount = x.PaymentAmount, paymentCycle = x.PaymentCycle, remarks = x.Remarks, attachment = x.Attachment, createUser = x.CreateUser, createUserName = "", createTime = x.CreateTime, updateUser = x.UpdateUser, updateUserName = "", updateTime = x.UpdateTime, isEffective = x.IsEffective }) .MergeTable() .OrderBy(sidx + " " + sort) .ToPagedListAsync(input.currentPage, input.pageSize); // 补充用户信息 var userIds = data.list.SelectMany(x => new[] { x.createUser, x.updateUser }) .Where(x => !string.IsNullOrEmpty(x)) .Distinct() .ToList(); if (userIds.Any()) { var userList = await _db.Queryable() .Where(x => userIds.Contains(x.Id)) .Select(x => new { x.Id, x.RealName }) .ToListAsync(); var userDict = userList.ToDictionary(k => k.Id, v => v.RealName); foreach (var item in data.list) { if (!string.IsNullOrEmpty(item.createUser) && userDict.ContainsKey(item.createUser)) item.createUserName = userDict[item.createUser]; if (!string.IsNullOrEmpty(item.updateUser) && userDict.ContainsKey(item.updateUser)) item.updateUserName = userDict[item.updateUser]; } } return PageResult.SqlSugarPageResult(data); } catch (Exception ex) { _logger.LogError(ex, "获取合同列表失败"); throw NCCException.Oh($"获取合同列表失败:{ex.Message}"); } } #endregion #region 获取合同详情 /// /// 获取合同详情 /// /// /// 根据合同ID获取合同详细信息,包括月租明细列表 /// /// 示例请求: /// ``` /// GET /api/Extend/LqContract/GetInfo?id=合同ID /// ``` /// /// 合同ID /// 合同详情 /// 查询成功 /// 合同ID不能为空 /// 合同不存在 /// 服务器错误 [HttpGet("GetInfo")] public async Task GetInfoAsync([FromQuery] string id) { try { if (string.IsNullOrWhiteSpace(id)) { throw NCCException.Oh("合同ID不能为空"); } // 查询合同 var contract = await _db.Queryable() .Where(x => x.Id == id && x.IsEffective == StatusEnum.有效.GetHashCode()) .FirstAsync(); if (contract == null) { throw NCCException.Oh("合同不存在或已失效"); } // 查询月租明细 var rentDetails = await _db.Queryable() .Where(x => x.ContractId == id && x.IsEffective == StatusEnum.有效.GetHashCode()) .OrderBy(x => x.PaymentMonth) .Select(x => new LqContractRentDetailListOutput { id = x.Id, contractId = x.ContractId, paymentMonth = x.PaymentMonth, dueDate = x.DueDate, dueAmount = x.DueAmount, isPaid = x.IsPaid, actualPaymentDate = x.ActualPaymentDate, actualPaymentAmount = x.ActualPaymentAmount, remarks = x.Remarks, createUser = x.CreateUser, createUserName = "", createTime = x.CreateTime, updateUser = x.UpdateUser, updateUserName = "", updateTime = x.UpdateTime, isEffective = x.IsEffective }) .ToListAsync(); // 补充用户信息 var userIds = new List { contract.CreateUser, contract.UpdateUser }; userIds.AddRange(rentDetails.SelectMany(x => new[] { x.createUser, x.updateUser }).Where(x => !string.IsNullOrEmpty(x))); string createUserName = ""; string updateUserName = ""; if (userIds.Any()) { var userList = await _db.Queryable() .Where(x => userIds.Distinct().Contains(x.Id)) .Select(x => new { x.Id, x.RealName }) .ToListAsync(); var userDict = userList.ToDictionary(k => k.Id, v => v.RealName); if (!string.IsNullOrEmpty(contract.CreateUser) && userDict.ContainsKey(contract.CreateUser)) createUserName = userDict[contract.CreateUser]; if (!string.IsNullOrEmpty(contract.UpdateUser) && userDict.ContainsKey(contract.UpdateUser)) updateUserName = userDict[contract.UpdateUser]; foreach (var detail in rentDetails) { if (!string.IsNullOrEmpty(detail.createUser) && userDict.ContainsKey(detail.createUser)) detail.createUserName = userDict[detail.createUser]; if (!string.IsNullOrEmpty(detail.updateUser) && userDict.ContainsKey(detail.updateUser)) detail.updateUserName = userDict[detail.updateUser]; } } return new LqContractInfoOutput { id = contract.Id, storeId = contract.StoreId, storeName = contract.StoreName, title = contract.Title, category = contract.Category, tenantName = contract.TenantName, contractStartDate = contract.ContractStartDate, contractEndDate = contract.ContractEndDate, reminderDays = contract.ReminderDays, deposit = contract.Deposit, nextPaymentDate = contract.NextPaymentDate, monthlyRent = contract.MonthlyRent, paymentAmount = contract.PaymentAmount, paymentCycle = contract.PaymentCycle, remarks = contract.Remarks, attachment = contract.Attachment, createUser = contract.CreateUser, createUserName = createUserName, createTime = contract.CreateTime, updateUser = contract.UpdateUser, updateUserName = updateUserName, updateTime = contract.UpdateTime, isEffective = contract.IsEffective, rentDetails = rentDetails }; } catch (Exception ex) { _logger.LogError(ex, "获取合同详情失败"); throw NCCException.Oh($"获取合同详情失败:{ex.Message}"); } } #endregion #region 获取月租明细列表 /// /// 获取月租明细列表 /// /// /// 根据合同ID获取该合同的所有月租明细 /// /// 示例请求: /// ``` /// GET /api/Extend/LqContract/GetRentDetails?contractId=合同ID /// ``` /// /// 合同ID /// 月租明细列表 /// 查询成功 /// 合同ID不能为空 /// 服务器错误 [HttpGet("GetRentDetails")] public async Task GetRentDetailsAsync([FromQuery] string contractId) { try { if (string.IsNullOrWhiteSpace(contractId)) { throw NCCException.Oh("合同ID不能为空"); } var rentDetails = await _db.Queryable() .Where(x => x.ContractId == contractId && x.IsEffective == StatusEnum.有效.GetHashCode()) .OrderBy(x => x.PaymentMonth) .Select(x => new LqContractRentDetailListOutput { id = x.Id, contractId = x.ContractId, paymentMonth = x.PaymentMonth, dueDate = x.DueDate, dueAmount = x.DueAmount, isPaid = x.IsPaid, actualPaymentDate = x.ActualPaymentDate, actualPaymentAmount = x.ActualPaymentAmount, remarks = x.Remarks, createUser = x.CreateUser, createUserName = "", createTime = x.CreateTime, updateUser = x.UpdateUser, updateUserName = "", updateTime = x.UpdateTime, isEffective = x.IsEffective }) .ToListAsync(); // 补充用户信息 var userIds = rentDetails.SelectMany(x => new[] { x.createUser, x.updateUser }) .Where(x => !string.IsNullOrEmpty(x)) .Distinct() .ToList(); if (userIds.Any()) { var userList = await _db.Queryable() .Where(x => userIds.Contains(x.Id)) .Select(x => new { x.Id, x.RealName }) .ToListAsync(); var userDict = userList.ToDictionary(k => k.Id, v => v.RealName); foreach (var detail in rentDetails) { if (!string.IsNullOrEmpty(detail.createUser) && userDict.ContainsKey(detail.createUser)) detail.createUserName = userDict[detail.createUser]; if (!string.IsNullOrEmpty(detail.updateUser) && userDict.ContainsKey(detail.updateUser)) detail.updateUserName = userDict[detail.updateUser]; } } return rentDetails; } catch (Exception ex) { _logger.LogError(ex, "获取月租明细列表失败"); throw NCCException.Oh($"获取月租明细列表失败:{ex.Message}"); } } #endregion #region 标记明细已缴费 /// /// 标记月租明细已缴费 /// /// /// 标记某条月租明细已缴费,记录实际缴费时间和金额 /// /// 示例请求: /// ```json /// { /// "id": "明细ID", /// "actualPaymentDate": "2025-01-15T00:00:00", /// "actualPaymentAmount": 3000.00, /// "remarks": "已缴费" /// } /// ``` /// /// 标记已缴费输入 /// 标记结果 /// 标记成功 /// 明细不存在或已缴费 /// 服务器错误 [HttpPut("MarkRentDetailPaid")] public async Task MarkRentDetailPaidAsync([FromBody] LqContractRentDetailMarkPaidInput input) { try { // 查询明细 var detail = await _db.Queryable() .Where(x => x.Id == input.Id && x.IsEffective == StatusEnum.有效.GetHashCode()) .FirstAsync(); if (detail == null) { throw NCCException.Oh("月租明细不存在或已失效"); } if (detail.IsPaid == 1) { throw NCCException.Oh("该明细已标记为已缴费"); } _db.Ado.BeginTran(); try { // 更新明细 detail.IsPaid = 1; detail.ActualPaymentDate = input.ActualPaymentDate; detail.ActualPaymentAmount = input.ActualPaymentAmount; detail.Remarks = input.Remarks; detail.UpdateUser = _userManager.UserId; detail.UpdateTime = DateTime.Now; var updateCount = await _db.Updateable(detail).ExecuteCommandAsync(); if (updateCount <= 0) { throw NCCException.Oh("标记已缴费失败"); } // 重新计算下次应交时间 await CalculateNextPaymentDate(detail.ContractId); _db.Ado.CommitTran(); } catch { _db.Ado.RollbackTran(); throw; } } catch (Exception ex) { _db.Ado.RollbackTran(); _logger.LogError(ex, "标记明细已缴费失败"); throw NCCException.Oh($"标记失败:{ex.Message}"); } } #endregion #region 根据合同id获取合同成本列表 /// /// 根据合同id获取合同成本列表 /// /// /// [HttpGet("GetContractCostList")] public async Task GetContractCostListAsync([FromQuery] string contractId) { try { if (string.IsNullOrWhiteSpace(contractId)) { throw NCCException.Oh("合同ID不能为空"); } var costList = await _db.Queryable() .Where(x => x.ContractId == contractId && x.IsEffective == StatusEnum.有效.GetHashCode()) .ToListAsync(); return costList; } catch (Exception ex) { _logger.LogError(ex, "获取合同成本列表失败"); throw NCCException.Oh($"获取合同成本列表失败:{ex.Message}"); } } #endregion #region 私有方法 /// /// 自动生成月租明细 /// /// 合同ID /// 合同起始日期 /// 合同结束日期 /// 交租周期(月) /// 缴租金额 private async Task GenerateRentDetailsAsync(string contractId, DateTime contractStartDate, DateTime contractEndDate, int paymentCycle, decimal paymentAmount) { var details = new List(); var currentDate = contractStartDate.Date; // 从合同起始日期开始,每隔交租周期生成一条明细 while (currentDate <= contractEndDate) { // 应缴月份:当前日期的月份第一天(格式:YYYY-MM-01) var paymentMonth = new DateTime(currentDate.Year, currentDate.Month, 1); // 应缴日期:应缴月份的第一天(根据业务规则,应缴日期为应缴月份的第一天) var dueDate = paymentMonth; var detail = new LqContractRentDetailEntity { Id = YitIdHelper.NextId().ToString(), ContractId = contractId, PaymentMonth = paymentMonth, DueDate = dueDate, DueAmount = paymentAmount, IsPaid = 0, CreateUser = _userManager.UserId, CreateTime = DateTime.Now, IsEffective = StatusEnum.有效.GetHashCode() }; details.Add(detail); // 计算下一个交租日期(加上交租周期) currentDate = currentDate.AddMonths(paymentCycle); } // 批量插入 if (details.Any()) { await _db.Insertable(details).ExecuteCommandAsync(); } } /// /// 生成按月成本记录 /// /// 合同ID /// 门店ID /// 店名 /// 分类 /// 合同起始日期 /// 合同结束日期 /// 交租周期(月) /// 缴租金额 private async Task GenerateMonthlyCostAsync(string contractId, string storeId, string storeName, string category, DateTime contractStartDate, DateTime contractEndDate, int paymentCycle, decimal paymentAmount) { // 计算每个月成本 = 缴租金额 / 交租周期 var monthlyCost = paymentAmount / paymentCycle; var costRecords = new List(); var currentMonth = new DateTime(contractStartDate.Year, contractStartDate.Month, 1); // 从合同起始月份开始,逐月生成成本记录,直到合同结束月份 while (currentMonth <= contractEndDate) { var costRecord = new LqContractMonthlyCostEntity { Id = YitIdHelper.NextId().ToString(), ContractId = contractId, StoreId = storeId, StoreName = storeName, Category = category, Month = currentMonth, MonthlyCost = monthlyCost, PaymentCycle = paymentCycle, PaymentAmount = paymentAmount, IsEffective = StatusEnum.有效.GetHashCode(), CreateUser = _userManager.UserId, CreateTime = DateTime.Now }; costRecords.Add(costRecord); // 计算下一个月 currentMonth = currentMonth.AddMonths(1); } // 批量插入 if (costRecords.Any()) { await _db.Insertable(costRecords).ExecuteCommandAsync(); } } /// /// 计算下次应交时间(匿名方法) /// /// 合同ID private async Task CalculateNextPaymentDate(string contractId) { // 查询合同信息 var contract = await _db.Queryable() .Where(x => x.Id == contractId && x.IsEffective == StatusEnum.有效.GetHashCode()) .FirstAsync(); if (contract == null) { return; } // 查询该合同未缴费的明细,按应缴日期升序排列 var unpaidDetail = await _db.Queryable() .Where(x => x.ContractId == contractId && x.IsEffective == StatusEnum.有效.GetHashCode() && x.IsPaid == 0) .OrderBy(x => x.DueDate) .FirstAsync(); if (unpaidDetail != null) { // 计算:下次应交时间 = 最早应缴日期 - 提前提醒天数 var nextPaymentDate = unpaidDetail.DueDate.AddDays(-contract.ReminderDays); // 更新合同的下次应交时间 contract.NextPaymentDate = nextPaymentDate; contract.UpdateUser = _userManager.UserId; contract.UpdateTime = DateTime.Now; await _db.Updateable(contract).ExecuteCommandAsync(); } else { // 如果没有未缴费的明细,清空下次应交时间 contract.NextPaymentDate = null; contract.UpdateUser = _userManager.UserId; contract.UpdateTime = DateTime.Now; await _db.Updateable(contract).ExecuteCommandAsync(); } } #endregion #region 获取合同成本按月统计 /// /// 获取合同成本按月统计 /// /// /// 根据合同ID、门店ID、月份等条件查询合同成本按月统计数据 /// /// 示例请求: /// ``` /// GET /api/Extend/LqContract/GetMonthlyCost?contractId=合同ID&storeId=门店ID&startMonth=2025-01&endMonth=2025-12 /// ``` /// /// 参数说明: /// - contractId: 合同ID(可选) /// - storeId: 门店ID(可选) /// - startMonth: 开始月份(格式:YYYY-MM,可选) /// - endMonth: 结束月份(格式:YYYY-MM,可选) /// /// 合同ID(可选) /// 门店ID(可选) /// 分类(可选) /// 开始月份(格式:YYYY-MM,可选) /// 结束月份(格式:YYYY-MM,可选) /// 合同成本按月统计列表 /// 查询成功 /// 服务器错误 [HttpGet("GetMonthlyCost")] public async Task GetMonthlyCostAsync( [FromQuery] string contractId = null, [FromQuery] string storeId = null, [FromQuery] string category = null, [FromQuery] string startMonth = null, [FromQuery] string endMonth = null) { try { var query = _db.Queryable() .Where(x => x.IsEffective == StatusEnum.有效.GetHashCode()) .WhereIF(!string.IsNullOrWhiteSpace(contractId), x => x.ContractId == contractId) .WhereIF(!string.IsNullOrWhiteSpace(storeId), x => x.StoreId == storeId) .WhereIF(!string.IsNullOrWhiteSpace(category), x => x.Category == category); // 处理月份范围筛选 if (!string.IsNullOrWhiteSpace(startMonth)) { if (DateTime.TryParseExact(startMonth, "yyyy-MM", null, DateTimeStyles.None, out DateTime startDate)) { var startMonthDate = new DateTime(startDate.Year, startDate.Month, 1); query = query.Where(x => x.Month >= startMonthDate); } } if (!string.IsNullOrWhiteSpace(endMonth)) { if (DateTime.TryParseExact(endMonth, "yyyy-MM", null, DateTimeStyles.None, out DateTime endDate)) { var endMonthDate = new DateTime(endDate.Year, endDate.Month, DateTime.DaysInMonth(endDate.Year, endDate.Month)); query = query.Where(x => x.Month <= endMonthDate); } } var data = await query .OrderBy(x => x.Month) .Select(x => new { id = x.Id, contractId = x.ContractId, storeId = x.StoreId, storeName = x.StoreName, category = x.Category, month = x.Month.ToString("yyyy-MM"), monthlyCost = x.MonthlyCost, paymentCycle = x.PaymentCycle, paymentAmount = x.PaymentAmount, createTime = x.CreateTime }) .ToListAsync(); return new { code = 200, msg = "查询成功", data = data }; } catch (Exception ex) { _logger.LogError(ex, "获取合同成本按月统计失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } /// /// 按门店和月份统计合同成本 /// /// /// 根据门店ID和月份统计该门店的合同成本总和 /// /// 示例请求: /// ``` /// GET /api/Extend/LqContract/GetStoreMonthlyCost?storeId=门店ID&month=2025-11 /// ``` /// /// 门店ID /// 月份(格式:YYYY-MM) /// 该门店该月的合同成本总和 /// 查询成功 /// 参数错误 /// 服务器错误 [HttpGet("GetStoreMonthlyCost")] public async Task GetStoreMonthlyCostAsync( [FromQuery] string storeId, [FromQuery] string month) { try { if (string.IsNullOrWhiteSpace(storeId)) { throw NCCException.Oh("门店ID不能为空"); } if (string.IsNullOrWhiteSpace(month)) { throw NCCException.Oh("月份不能为空"); } if (!DateTime.TryParseExact(month, "yyyy-MM", null, DateTimeStyles.None, out DateTime monthDate)) { throw NCCException.Oh("月份格式错误,应为 YYYY-MM"); } var monthStart = new DateTime(monthDate.Year, monthDate.Month, 1); var totalCost = await _db.Queryable() .Where(x => x.StoreId == storeId && x.Month == monthStart && x.IsEffective == StatusEnum.有效.GetHashCode()) .SumAsync(x => x.MonthlyCost); return new { code = 200, msg = "查询成功", data = new { storeId = storeId, month = month, totalCost = totalCost } }; } catch (Exception ex) { _logger.LogError(ex, "按门店和月份统计合同成本失败"); throw NCCException.Oh($"查询失败:{ex.Message}"); } } #endregion #region 统计门店合同费用 /// /// 统计门店合同费用 /// /// /// 根据门店ID和月份统计该门店的合同费用,支持按分类统计(租门店、员工宿舍、车辆、场所等) /// /// 示例请求: /// ```json /// { /// "storeId": "1649328471923847168", /// "year": 2025, /// "month": 1, /// "categories": ["租门店", "员工宿舍", "车辆", "场所"] /// } /// ``` /// /// 参数说明: /// - storeId: 门店ID(必填) /// - year: 统计年份(可选,不传则统计所有年份) /// - month: 统计月份(可选,1-12,不传则统计所有月份;如果只传月份不传年份,则使用当前年份) /// - categories: 分类列表(可选,不传则统计所有分类) /// /// 时间范围说明: /// - 如果同时提供年份和月份:统计指定月份的数据 /// - 如果只提供年份:统计整年的数据 /// - 如果都不提供:统计所有时间的数据 /// /// 返回数据说明: /// - storeId: 门店ID /// - storeName: 门店名称 /// - year: 统计年份 /// - month: 统计月份 /// - totalAmount: 总费用(所有分类的费用总和) /// - categoryDetails: 按分类统计的费用明细 /// - category: 分类名称 /// - amount: 该分类的费用总额 /// - detailCount: 该分类的明细数量 /// - details: 明细列表 /// /// 统计输入 /// 费用统计结果 /// 统计成功 /// 请求参数错误 /// 服务器错误 [HttpPost("GetExpenseStatistics")] public async Task GetExpenseStatisticsAsync([FromBody] ContractExpenseStatisticsInput input) { try { // 验证年份(如果提供了年份) if (input.Year.HasValue && (input.Year < 2020 || input.Year > 2100)) { throw NCCException.Oh("年份必须在2020-2100之间"); } // 验证月份(如果提供了月份) if (input.Month.HasValue && (input.Month < 1 || input.Month > 12)) { throw NCCException.Oh("月份必须在1-12之间"); } // 如果提供了月份但没有提供年份,使用当前年份 if (input.Month.HasValue && !input.Year.HasValue) { input.Year = DateTime.Now.Year; } // 构建时间范围 DateTime? monthStart = null; DateTime? monthEnd = null; if (input.Year.HasValue && input.Month.HasValue) { // 统计指定月份 var statisticsMonth = new DateTime(input.Year.Value, input.Month.Value, 1); monthStart = statisticsMonth; monthEnd = statisticsMonth.AddMonths(1).AddDays(-1); } else if (input.Year.HasValue) { // 只提供了年份,统计整年 monthStart = new DateTime(input.Year.Value, 1, 1); monthEnd = new DateTime(input.Year.Value, 12, 31, 23, 59, 59); } // 如果年份和月份都没提供,monthStart 和 monthEnd 保持为 null,表示不按时间过滤 // 查询门店信息 var store = await _db.Queryable() .Where(x => x.Id == input.StoreId) .Select(x => new { x.Dm }) .FirstAsync(); if (store == null) { throw NCCException.Oh("门店不存在"); } // 查询该门店在指定时间范围的月租明细 var detailsQuery = _db.Queryable( (detail, contract) => new JoinQueryInfos( JoinType.Inner, detail.ContractId == contract.Id)) .Where((detail, contract) => contract.StoreId == input.StoreId && contract.IsEffective == StatusEnum.有效.GetHashCode() && detail.IsEffective == StatusEnum.有效.GetHashCode()); // 如果提供了时间范围,则添加时间过滤条件 if (monthStart.HasValue && monthEnd.HasValue) { detailsQuery = detailsQuery.Where((detail, contract) => detail.PaymentMonth >= monthStart.Value && detail.PaymentMonth <= monthEnd.Value); } // 如果指定了分类,则添加分类过滤条件 var details = await detailsQuery .WhereIF(input.Categories != null && input.Categories.Length > 0, (detail, contract) => contract.Category != null && input.Categories.Contains(contract.Category)) .Select((detail, contract) => new { DetailId = detail.Id, ContractId = contract.Id, ContractTitle = contract.Title, Category = contract.Category ?? "未分类", DueAmount = detail.DueAmount, IsPaid = detail.IsPaid, ActualPaymentAmount = detail.ActualPaymentAmount }) .ToListAsync(); // 按分类分组统计 var categoryGroups = details.GroupBy(x => x.Category).ToList(); var categoryDetails = new List(); decimal totalAmount = 0; foreach (var group in categoryGroups) { var categoryName = group.Key; var categoryItems = group.ToList(); var categoryAmount = categoryItems.Sum(x => x.DueAmount); totalAmount += categoryAmount; var expenseDetails = categoryItems.Select(x => new ExpenseDetailItem { detailId = x.DetailId, contractId = x.ContractId, contractTitle = x.ContractTitle, category = x.Category, dueAmount = x.DueAmount, isPaid = x.IsPaid, actualPaymentAmount = x.ActualPaymentAmount }).ToList(); categoryDetails.Add(new CategoryExpenseDetail { category = categoryName, amount = categoryAmount, detailCount = categoryItems.Count, details = expenseDetails }); } // 按分类名称排序 categoryDetails = categoryDetails.OrderBy(x => x.category).ToList(); return new ContractExpenseStatisticsOutput { storeId = input.StoreId, storeName = store.Dm ?? "", year = input.Year ?? 0, month = input.Month ?? 0, totalAmount = totalAmount, categoryDetails = categoryDetails }; } catch (Exception ex) { _logger.LogError(ex, "统计门店合同费用失败"); throw NCCException.Oh($"统计失败:{ex.Message}"); } } #endregion } }