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
#region 付款提醒列表
///
/// 获取付款前提醒列表
///
///
/// 查询需要提醒付款的合同列表
///
/// 查询逻辑:
/// 1. 查询所有有效的合同,且 F_NextPaymentDate 不为空
/// 2. 如果 OnlyOverdue 为 true,只返回 F_NextPaymentDate 小于等于当前日期的合同
/// 3. 如果 OnlyOverdue 为 false,返回所有有提醒时间的合同
/// 4. 关联查询未缴费的明细,获取应缴日期和应缴金额
/// 5. 计算距离应缴日期的天数
///
/// 示例请求:
/// ```json
/// {
/// "storeId": "门店ID(可选)",
/// "onlyOverdue": true
/// }
/// ```
///
/// 参数说明:
/// - storeId: 门店ID,可选,用于筛选特定门店
/// - onlyOverdue: 是否只查询已到期的提醒,默认true
///
/// 查询输入
/// 付款提醒列表
/// 查询成功
/// 服务器错误
[HttpPost("GetPaymentReminderList")]
public async Task> GetPaymentReminderListAsync([FromBody] PaymentReminderListInput input = null)
{
try
{
if (input == null)
{
input = new PaymentReminderListInput();
}
var now = DateTime.Now.Date;
// 查询需要提醒的合同
var contracts = await _db.Queryable()
.Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
.Where(x => x.NextPaymentDate != null)
.WhereIF(input.OnlyOverdue, x => x.NextPaymentDate.Value.Date <= now)
.WhereIF(!string.IsNullOrWhiteSpace(input.StoreId), x => x.StoreId == input.StoreId)
.OrderBy(x => x.NextPaymentDate)
.ToListAsync();
if (!contracts.Any())
{
return new List();
}
var contractIds = contracts.Select(x => x.Id).ToList();
// 查询所有未缴费的明细
var unpaidDetails = await _db.Queryable()
.Where(x => contractIds.Contains(x.ContractId))
.Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
.Where(x => x.IsPaid == 0)
.OrderBy(x => x.DueDate)
.ToListAsync();
// 按合同ID分组,获取每个合同最早未缴费的明细
var contractDetailDict = unpaidDetails
.GroupBy(x => x.ContractId)
.ToDictionary(
g => g.Key,
g => g.OrderBy(x => x.DueDate).First()
);
// 构建返回结果
var result = new List();
foreach (var contract in contracts)
{
if (contractDetailDict.ContainsKey(contract.Id))
{
var detail = contractDetailDict[contract.Id];
var daysUntilDue = (detail.DueDate.Date - now).Days;
var isOverdue = detail.DueDate.Date < now;
result.Add(new PaymentReminderListOutput
{
ContractId = contract.Id,
DetailId = detail.Id,
StoreId = contract.StoreId,
StoreName = contract.StoreName,
Title = contract.Title,
Category = contract.Category,
TenantName = contract.TenantName,
PaymentDate = detail.DueDate,
PaymentAmount = detail.DueAmount,
PaymentMonth = detail.PaymentMonth.ToString("yyyy-MM"),
ReminderDate = contract.NextPaymentDate,
ReminderDays = contract.ReminderDays,
DaysUntilPayment = daysUntilDue,
IsOverdue = isOverdue,
PaymentCycle = contract.PaymentCycle,
Remarks = detail.Remarks
});
}
}
return result.OrderBy(x => x.PaymentDate).ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "获取付款提醒列表失败");
throw NCCException.Oh($"获取付款提醒列表失败:{ex.Message}");
}
}
#endregion
}
}