418 lines
15 KiB
C#
Raw Normal View History

2026-03-01 22:32:44 +08:00
using YD_Event.Application.Project.Dtos;
using YD_Event.Core.Entities;
2026-04-12 16:11:20 +08:00
using YD_Event.Core.Enum;
2026-03-08 15:46:19 +08:00
using Mapster;
using SqlSugar;
using Microsoft.Extensions.Logging;
2026-04-12 16:11:20 +08:00
using Easy.Core;
2026-03-01 22:32:44 +08:00
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;
2026-08-11 23:41:18 +08:00
private readonly ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> _scoreRuleRepository;
2026-03-01 22:32:44 +08:00
private readonly IIdGenerator _idGenerator;
private readonly ILogger<ProjectService> _logger;
2026-03-01 22:32:44 +08:00
public ProjectService(
ISqlSugarRepository<YD_Event.Core.Entities.Project> repository,
ISqlSugarRepository<ProjectGroup> groupRepository,
ISqlSugarRepository<EventProject> eventProjectRepository,
ISqlSugarRepository<EventParticipationRecord> participationRecordRepository,
2026-08-11 23:41:18 +08:00
ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> scoreRuleRepository,
IIdGenerator idGenerator,
ILogger<ProjectService> logger) : base(repository)
2026-03-01 22:32:44 +08:00
{
_repository = repository;
_groupRepository = groupRepository;
_eventProjectRepository = eventProjectRepository;
_participationRecordRepository = participationRecordRepository;
2026-08-11 23:41:18 +08:00
_scoreRuleRepository = scoreRuleRepository;
2026-03-01 22:32:44 +08:00
_idGenerator = idGenerator;
_logger = logger;
2026-03-01 22:32:44 +08:00
}
/// <summary>
/// 项目分页查询
/// </summary>
[DisplayName("项目分页查询")]
2026-03-08 15:46:19 +08:00
public async Task<PageResult<ProjectDto>> Page(ProjectQueryInput input)
2026-03-01 22:32:44 +08:00
{
2026-03-08 15:46:19 +08:00
var query = _repository.AsQueryable()
2026-04-12 16:11:20 +08:00
.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);
2026-03-08 15:46:19 +08:00
2026-04-12 16:11:20 +08:00
var list = await query.Select(p => new ProjectDto
2026-03-08 15:46:19 +08:00
{
Id = p.Id,
Name = p.Name,
2026-04-12 16:11:20 +08:00
ProjectType = (long)p.ProjectType,
CompletionCountType = p.CompletionCountType,
2026-03-08 15:46:19 +08:00
CompletionCount = p.CompletionCount,
CompletionCountMin = p.CompletionCountMin,
CompletionCountMax = p.CompletionCountMax,
2026-03-08 15:46:19 +08:00
Fee = p.Fee,
DurationMinutes = p.DurationMinutes,
DurationSeconds = p.DurationSeconds,
2026-08-11 23:41:18 +08:00
ScoreRuleId = p.ScoreRuleId,
2026-07-18 13:12:58 +08:00
Remark = p.Remark,
2026-03-08 15:46:19 +08:00
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);
2026-04-12 16:11:20 +08:00
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}人";
}
2026-04-12 16:11:20 +08:00
}
2026-08-11 23:41:18 +08:00
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);
2026-04-12 16:11:20 +08:00
return list;
2026-03-01 22:32:44 +08:00
}
/// <summary>
/// 添加项目
/// </summary>
[DisplayName("添加项目")]
[UnitOfWork]
public async Task Add(AddProjectInput input)
{
2026-08-11 23:41:18 +08:00
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("完成人数最小值不能大于最大值");
}
}
2026-03-01 22:32:44 +08:00
var entity = input.Adapt<YD_Event.Core.Entities.Project>();
2026-03-08 15:46:19 +08:00
entity.Id = _idGenerator.NewLong();
entity.CreatedTime = DateTime.Now;
entity.Status = Core.Enum.AvailabilityStatus.Enable;
2026-03-01 22:32:44 +08:00
await _repository.InsertAsync(entity);
if (input.Groups != null && input.Groups.Any())
{
2026-03-08 15:46:19 +08:00
var groups = input.Groups.Select(g =>
2026-03-01 22:32:44 +08:00
{
2026-03-08 15:46:19 +08:00
var group = g.Adapt<ProjectGroup>();
group.Id = _idGenerator.NewLong();
group.ProjectId = entity.Id;
return group;
}).ToList();
2026-03-01 22:32:44 +08:00
await _groupRepository.InsertRangeAsync(groups);
}
}
/// <summary>
/// 更新项目
/// </summary>
[DisplayName("更新项目")]
[UnitOfWork]
public async Task Update(UpdateProjectInput input)
{
2026-08-11 23:41:18 +08:00
await ValidateScoreRule(input.ScoreRuleId);
2026-03-01 22:32:44 +08:00
var entity = await _repository.GetByIdAsync(input.Id);
2026-03-08 15:46:19 +08:00
if (entity == null) throw new InvalidOperationException("项目不存在");
2026-03-01 22:32:44 +08:00
// 验证完成人数字段
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("完成人数最小值不能大于最大值");
}
}
2026-03-01 22:32:44 +08:00
input.Adapt(entity);
2026-03-08 15:46:19 +08:00
entity.UpdatedTime = DateTime.Now;
2026-03-01 22:32:44 +08:00
await _repository.UpdateAsync(entity);
2026-06-06 23:34:12 +08:00
await _groupRepository.SoftDeleteAsync(x => x.ProjectId == entity.Id);
2026-03-01 22:32:44 +08:00
if (input.Groups != null && input.Groups.Any())
{
2026-03-08 15:46:19 +08:00
var groups = input.Groups.Select(g =>
2026-03-01 22:32:44 +08:00
{
2026-03-08 15:46:19 +08:00
var group = g.Adapt<ProjectGroup>();
group.Id = _idGenerator.NewLong();
group.ProjectId = entity.Id;
return group;
}).ToList();
2026-03-01 22:32:44 +08:00
await _groupRepository.InsertRangeAsync(groups);
}
}
/// <summary>
/// 删除项目
/// </summary>
[DisplayName("删除项目")]
[UnitOfWork]
2026-03-08 15:46:19 +08:00
public async Task Delete(List<long> ids)
2026-03-01 22:32:44 +08:00
{
2026-04-10 22:39:32 +08:00
_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("部分项目已有报名数据,为保证数据完整性,无法删除。");
}
}
2026-06-06 23:34:12 +08:00
// 执行删除 - 软删
await _repository.SoftDeleteByIdsAsync(ids);
await _groupRepository.SoftDeleteAsync(x => ids.Contains(x.ProjectId));
2026-04-10 22:39:32 +08:00
_logger.LogInformation("删除项目成功:项目ID列表={Ids}", string.Join(",", ids));
2026-03-01 22:32:44 +08:00
}
/// <summary>
/// 获取项目详情
/// </summary>
[DisplayName("获取项目详情")]
2026-03-08 15:46:19 +08:00
public async Task<ProjectDto> Detail(long id)
2026-03-01 22:32:44 +08:00
{
var p = await _repository.GetByIdAsync(id);
2026-03-08 15:46:19 +08:00
if (p == null) throw new InvalidOperationException("项目不存在");
2026-03-01 22:32:44 +08:00
var dto = p.Adapt<ProjectDto>();
2026-04-12 16:11:20 +08:00
dto.ProjectType = (long)p.ProjectType;
dto.ProjectTypeName = p.ProjectType.Description();
2026-08-11 23:41:18 +08:00
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}人";
}
2026-03-08 15:46:19 +08:00
var groups = await _groupRepository.GetListAsync(x => x.ProjectId == id);
dto.Groups = groups.Adapt<List<ProjectGroupDto>>();
2026-03-01 22:32:44 +08:00
return dto;
}
2026-08-11 23:41:18 +08:00
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;
}
2026-03-01 22:32:44 +08:00
}