418 lines
15 KiB
C#
418 lines
15 KiB
C#
using YD_Event.Application.Project.Dtos;
|
||
using YD_Event.Core.Entities;
|
||
using YD_Event.Core.Enum;
|
||
using Mapster;
|
||
using SqlSugar;
|
||
using Microsoft.Extensions.Logging;
|
||
using Easy.Core;
|
||
|
||
namespace YD_Event.Application.Project;
|
||
|
||
/// <summary>
|
||
/// 项目管理
|
||
/// </summary>
|
||
public class ProjectService : BaseService<YD_Event.Core.Entities.Project>, ITransient
|
||
{
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _repository;
|
||
private readonly ISqlSugarRepository<ProjectGroup> _groupRepository;
|
||
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
|
||
private readonly ISqlSugarRepository<EventParticipationRecord> _participationRecordRepository;
|
||
private readonly ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> _scoreRuleRepository;
|
||
private readonly IIdGenerator _idGenerator;
|
||
private readonly ILogger<ProjectService> _logger;
|
||
|
||
public ProjectService(
|
||
ISqlSugarRepository<YD_Event.Core.Entities.Project> repository,
|
||
ISqlSugarRepository<ProjectGroup> groupRepository,
|
||
ISqlSugarRepository<EventProject> eventProjectRepository,
|
||
ISqlSugarRepository<EventParticipationRecord> participationRecordRepository,
|
||
ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> scoreRuleRepository,
|
||
IIdGenerator idGenerator,
|
||
ILogger<ProjectService> logger) : base(repository)
|
||
{
|
||
_repository = repository;
|
||
_groupRepository = groupRepository;
|
||
_eventProjectRepository = eventProjectRepository;
|
||
_participationRecordRepository = participationRecordRepository;
|
||
_scoreRuleRepository = scoreRuleRepository;
|
||
_idGenerator = idGenerator;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 项目分页查询
|
||
/// </summary>
|
||
[DisplayName("项目分页查询")]
|
||
public async Task<PageResult<ProjectDto>> Page(ProjectQueryInput input)
|
||
{
|
||
var query = _repository.AsQueryable()
|
||
.WhereIF(!string.IsNullOrWhiteSpace(input.ProjectName), p => p.Name.Contains(input.ProjectName))
|
||
.WhereIF(input.ProjectType.HasValue, p => p.ProjectType == (ProjectTypeEnum)input.ProjectType.Value)
|
||
.OrderByDescending(p => p.Id);
|
||
|
||
var list = await query.Select(p => new ProjectDto
|
||
{
|
||
Id = p.Id,
|
||
Name = p.Name,
|
||
ProjectType = (long)p.ProjectType,
|
||
CompletionCountType = p.CompletionCountType,
|
||
CompletionCount = p.CompletionCount,
|
||
CompletionCountMin = p.CompletionCountMin,
|
||
CompletionCountMax = p.CompletionCountMax,
|
||
Fee = p.Fee,
|
||
DurationMinutes = p.DurationMinutes,
|
||
DurationSeconds = p.DurationSeconds,
|
||
ScoreRuleId = p.ScoreRuleId,
|
||
Remark = p.Remark,
|
||
VenueCount = p.VenueCount,
|
||
SortIndex = p.SortIndex,
|
||
Status = p.Status,
|
||
DeleteMark = p.DeleteMark,
|
||
CreatedUserId = p.CreatedUserId,
|
||
CreatedTime = p.CreatedTime,
|
||
UpdatedUserId = p.UpdatedUserId,
|
||
UpdatedTime = p.UpdatedTime
|
||
})
|
||
.ToPagedListAsync(input);
|
||
|
||
foreach (var item in list.Rows)
|
||
{
|
||
item.ProjectTypeName = ((ProjectTypeEnum)item.ProjectType).Description();
|
||
// 生成完成人数显示文本
|
||
if (item.CompletionCountType == 0)
|
||
{
|
||
item.CompletionCountDisplay = item.CompletionCount.HasValue ? $"{item.CompletionCount.Value}人" : "0人";
|
||
}
|
||
else
|
||
{
|
||
var min = item.CompletionCountMin ?? 0;
|
||
var max = item.CompletionCountMax ?? 0;
|
||
item.CompletionCountDisplay = $"{min}-{max}人";
|
||
}
|
||
}
|
||
var ruleIds = list.Rows.Where(x => x.ScoreRuleId.HasValue).Select(x => x.ScoreRuleId!.Value).Distinct().ToList();
|
||
var ruleMap = (await _scoreRuleRepository.AsQueryable().Where(x => ruleIds.Contains(x.Id)).ToListAsync()).ToDictionary(x => x.Id, x => x.RuleName);
|
||
foreach (var item in list.Rows) if (item.ScoreRuleId.HasValue) item.ScoreRuleName = ruleMap.GetValueOrDefault(item.ScoreRuleId.Value);
|
||
|
||
return list;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加项目
|
||
/// </summary>
|
||
[DisplayName("添加项目")]
|
||
[UnitOfWork]
|
||
public async Task Add(AddProjectInput input)
|
||
{
|
||
await ValidateScoreRule(input.ScoreRuleId);
|
||
// 验证完成人数字段
|
||
if (input.CompletionCountType == 0)
|
||
{
|
||
// 单一值模式
|
||
if (!input.CompletionCount.HasValue || input.CompletionCount.Value < 0)
|
||
{
|
||
throw Oops.Bah("请填写有效的完成人数");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 范围值模式
|
||
if (!input.CompletionCountMin.HasValue || !input.CompletionCountMax.HasValue)
|
||
{
|
||
throw Oops.Bah("请填写完成人数最小值和最大值");
|
||
}
|
||
if (input.CompletionCountMin.Value < 0 || input.CompletionCountMax.Value < 0)
|
||
{
|
||
throw Oops.Bah("完成人数不能为负数");
|
||
}
|
||
if (input.CompletionCountMin.Value > input.CompletionCountMax.Value)
|
||
{
|
||
throw Oops.Bah("完成人数最小值不能大于最大值");
|
||
}
|
||
}
|
||
|
||
var entity = input.Adapt<YD_Event.Core.Entities.Project>();
|
||
entity.Id = _idGenerator.NewLong();
|
||
entity.CreatedTime = DateTime.Now;
|
||
entity.Status = Core.Enum.AvailabilityStatus.Enable;
|
||
|
||
await _repository.InsertAsync(entity);
|
||
|
||
if (input.Groups != null && input.Groups.Any())
|
||
{
|
||
var groups = input.Groups.Select(g =>
|
||
{
|
||
var group = g.Adapt<ProjectGroup>();
|
||
group.Id = _idGenerator.NewLong();
|
||
group.ProjectId = entity.Id;
|
||
return group;
|
||
}).ToList();
|
||
await _groupRepository.InsertRangeAsync(groups);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新项目
|
||
/// </summary>
|
||
[DisplayName("更新项目")]
|
||
[UnitOfWork]
|
||
public async Task Update(UpdateProjectInput input)
|
||
{
|
||
await ValidateScoreRule(input.ScoreRuleId);
|
||
var entity = await _repository.GetByIdAsync(input.Id);
|
||
if (entity == null) throw new InvalidOperationException("项目不存在");
|
||
|
||
// 验证完成人数字段
|
||
if (input.CompletionCountType == 0)
|
||
{
|
||
// 单一值模式
|
||
if (!input.CompletionCount.HasValue || input.CompletionCount.Value < 0)
|
||
{
|
||
throw Oops.Bah("请填写有效的完成人数");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 范围值模式
|
||
if (!input.CompletionCountMin.HasValue || !input.CompletionCountMax.HasValue)
|
||
{
|
||
throw Oops.Bah("请填写完成人数最小值和最大值");
|
||
}
|
||
if (input.CompletionCountMin.Value < 0 || input.CompletionCountMax.Value < 0)
|
||
{
|
||
throw Oops.Bah("完成人数不能为负数");
|
||
}
|
||
if (input.CompletionCountMin.Value > input.CompletionCountMax.Value)
|
||
{
|
||
throw Oops.Bah("完成人数最小值不能大于最大值");
|
||
}
|
||
}
|
||
|
||
input.Adapt(entity);
|
||
entity.UpdatedTime = DateTime.Now;
|
||
await _repository.UpdateAsync(entity);
|
||
|
||
await _groupRepository.SoftDeleteAsync(x => x.ProjectId == entity.Id);
|
||
|
||
if (input.Groups != null && input.Groups.Any())
|
||
{
|
||
var groups = input.Groups.Select(g =>
|
||
{
|
||
var group = g.Adapt<ProjectGroup>();
|
||
group.Id = _idGenerator.NewLong();
|
||
group.ProjectId = entity.Id;
|
||
return group;
|
||
}).ToList();
|
||
await _groupRepository.InsertRangeAsync(groups);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除项目
|
||
/// </summary>
|
||
[DisplayName("删除项目")]
|
||
[UnitOfWork]
|
||
public async Task Delete(List<long> ids)
|
||
{
|
||
_logger.LogInformation("开始删除项目:项目ID列表={Ids}", string.Join(",", ids));
|
||
|
||
// 检查项目是否被赛事使用
|
||
var hasEventProjects = await _eventProjectRepository.IsAnyAsync(x => ids.Contains(x.ProjectId));
|
||
if (hasEventProjects)
|
||
{
|
||
_logger.LogWarning("删除失败:部分项目正在被赛事使用");
|
||
throw new InvalidOperationException("部分项目正在被赛事使用,无法删除。请先从赛事中移除这些项目。");
|
||
}
|
||
|
||
// 检查是否有报名数据关联
|
||
var eventProjectIds = await _eventProjectRepository.AsQueryable()
|
||
.Where(x => ids.Contains(x.ProjectId))
|
||
.Select(x => x.Id)
|
||
.ToListAsync();
|
||
|
||
if (eventProjectIds.Any())
|
||
{
|
||
var hasParticipations = await _participationRecordRepository.IsAnyAsync(x =>
|
||
eventProjectIds.Contains(x.ProjectId) && !x.DeleteMark);
|
||
|
||
if (hasParticipations)
|
||
{
|
||
_logger.LogWarning("删除失败:部分项目已有报名数据");
|
||
throw new InvalidOperationException("部分项目已有报名数据,为保证数据完整性,无法删除。");
|
||
}
|
||
}
|
||
|
||
// 执行删除 - 软删
|
||
await _repository.SoftDeleteByIdsAsync(ids);
|
||
await _groupRepository.SoftDeleteAsync(x => ids.Contains(x.ProjectId));
|
||
|
||
_logger.LogInformation("删除项目成功:项目ID列表={Ids}", string.Join(",", ids));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取项目详情
|
||
/// </summary>
|
||
[DisplayName("获取项目详情")]
|
||
public async Task<ProjectDto> Detail(long id)
|
||
{
|
||
var p = await _repository.GetByIdAsync(id);
|
||
if (p == null) throw new InvalidOperationException("项目不存在");
|
||
|
||
var dto = p.Adapt<ProjectDto>();
|
||
dto.ProjectType = (long)p.ProjectType;
|
||
dto.ProjectTypeName = p.ProjectType.Description();
|
||
if (p.ScoreRuleId.HasValue) dto.ScoreRuleName = (await _scoreRuleRepository.GetByIdAsync(p.ScoreRuleId.Value))?.RuleName;
|
||
|
||
// 生成完成人数显示文本
|
||
if (dto.CompletionCountType == 0)
|
||
{
|
||
dto.CompletionCountDisplay = dto.CompletionCount.HasValue ? $"{dto.CompletionCount.Value}人" : "0人";
|
||
}
|
||
else
|
||
{
|
||
var min = dto.CompletionCountMin ?? 0;
|
||
var max = dto.CompletionCountMax ?? 0;
|
||
dto.CompletionCountDisplay = $"{min}-{max}人";
|
||
}
|
||
|
||
var groups = await _groupRepository.GetListAsync(x => x.ProjectId == id);
|
||
dto.Groups = groups.Adapt<List<ProjectGroupDto>>();
|
||
|
||
return dto;
|
||
}
|
||
|
||
private async Task ValidateScoreRule(long? scoreRuleId)
|
||
{
|
||
if (!scoreRuleId.HasValue || !await _scoreRuleRepository.IsAnyAsync(x => x.Id == scoreRuleId.Value && x.Status == AvailabilityStatus.Enable))
|
||
throw Oops.Bah("请选择有效的计分规则");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 项目统计类型
|
||
/// </summary>
|
||
public enum ProjectStatisticType
|
||
{
|
||
/// <summary>
|
||
/// 参赛人数
|
||
/// </summary>
|
||
ParticipationCount = 1,
|
||
|
||
/// <summary>
|
||
/// 完成人数
|
||
/// </summary>
|
||
CompletionCount = 2
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取项目统计数据(单个项目)
|
||
/// </summary>
|
||
/// <param name="projectId">项目ID</param>
|
||
/// <param name="statisticType">统计类型</param>
|
||
/// <returns>统计数值</returns>
|
||
[DisplayName("获取项目统计数据")]
|
||
public async Task<int> GetProjectStatisticAsync(long projectId, ProjectStatisticType statisticType)
|
||
{
|
||
if (projectId <= 0)
|
||
{
|
||
_logger.LogWarning("Invalid projectId: {ProjectId}", projectId);
|
||
return 0;
|
||
}
|
||
|
||
var eventProjectIds = await _eventProjectRepository.AsQueryable()
|
||
.Where(ep => ep.ProjectId == projectId)
|
||
.Select(ep => ep.Id)
|
||
.ToListAsync();
|
||
|
||
if (!eventProjectIds.Any())
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
var query = _participationRecordRepository.AsQueryable()
|
||
.Where(pr => eventProjectIds.Contains(pr.ProjectId) && !pr.DeleteMark);
|
||
|
||
switch (statisticType)
|
||
{
|
||
case ProjectStatisticType.ParticipationCount:
|
||
query = query.Where(pr => pr.ParticipationStatus == ParticipationStatus.Participated || pr.ParticipationStatus == ParticipationStatus.Completed);
|
||
break;
|
||
|
||
case ProjectStatisticType.CompletionCount:
|
||
query = query.Where(pr => pr.ParticipationStatus == ParticipationStatus.Completed);
|
||
break;
|
||
|
||
default:
|
||
_logger.LogWarning("Unknown statistic type: {StatisticType}", statisticType);
|
||
return 0;
|
||
}
|
||
|
||
var count = await query.CountAsync();
|
||
return count;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量获取项目统计数据(性能优化,避免N+1查询)
|
||
/// </summary>
|
||
/// <param name="projectIds">项目ID列表</param>
|
||
/// <param name="statisticType">统计类型</param>
|
||
/// <returns>字典,Key为项目ID,Value为统计数值</returns>
|
||
[DisplayName("批量获取项目统计数据")]
|
||
public async Task<Dictionary<long, int>> GetProjectStatisticsBatchAsync(List<long> projectIds, ProjectStatisticType statisticType)
|
||
{
|
||
var result = new Dictionary<long, int>();
|
||
|
||
if (projectIds == null || !projectIds.Any())
|
||
{
|
||
return result;
|
||
}
|
||
|
||
foreach (var projectId in projectIds)
|
||
{
|
||
result[projectId] = 0;
|
||
}
|
||
|
||
var eventProjects = await _eventProjectRepository.AsQueryable()
|
||
.Where(ep => projectIds.Contains(ep.ProjectId))
|
||
.Select(ep => new { ep.Id, ep.ProjectId })
|
||
.ToListAsync();
|
||
|
||
if (!eventProjects.Any())
|
||
{
|
||
return result;
|
||
}
|
||
|
||
var eventProjectIds = eventProjects.Select(ep => ep.Id).ToList();
|
||
|
||
var query = _participationRecordRepository.AsQueryable()
|
||
.Where(pr => eventProjectIds.Contains(pr.ProjectId) && !pr.DeleteMark);
|
||
|
||
switch (statisticType)
|
||
{
|
||
case ProjectStatisticType.ParticipationCount:
|
||
query = query.Where(pr => pr.ParticipationStatus == ParticipationStatus.Participated || pr.ParticipationStatus == ParticipationStatus.Completed);
|
||
break;
|
||
|
||
case ProjectStatisticType.CompletionCount:
|
||
query = query.Where(pr => pr.ParticipationStatus == ParticipationStatus.Completed);
|
||
break;
|
||
|
||
default:
|
||
_logger.LogWarning("Unknown statistic type: {StatisticType}", statisticType);
|
||
return result;
|
||
}
|
||
|
||
var records = await query.ToListAsync();
|
||
|
||
foreach (var record in records)
|
||
{
|
||
var projectId = eventProjects.FirstOrDefault(ep => ep.Id == record.ProjectId)?.ProjectId;
|
||
if (projectId.HasValue && result.ContainsKey(projectId.Value))
|
||
{
|
||
result[projectId.Value]++;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|