diff --git a/YD_Event.Application/Certificate/CertificateGenerateService.cs b/YD_Event.Application/Certificate/CertificateGenerateService.cs index d151642..ad8e709 100644 --- a/YD_Event.Application/Certificate/CertificateGenerateService.cs +++ b/YD_Event.Application/Certificate/CertificateGenerateService.cs @@ -71,7 +71,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient } /// - /// 配置 PDF 字体 - 简化版,主要依赖系统预装字体 + /// 配置 PDF 字体 - 跨平台解决方案 /// private void ConfigurePdfFonts() { @@ -88,10 +88,10 @@ public class CertificateGenerateService : IDynamicApiController, ITransient // 设置 QuestPDF 许可证 QuestPDF.Settings.License = LicenseType.Community; - // QuestPDF 会自动发现和使用系统已安装的字体 - // 不需要手动注册,简化了流程 + // 尝试配置中文字体 + ConfigureChineseFonts(); - _logger.LogInformation("PDF 字体系统初始化完成 - 使用系统预装字体"); + _logger.LogInformation("PDF 字体系统初始化完成"); _fontConfigured = true; } catch (Exception ex) @@ -101,6 +101,66 @@ public class CertificateGenerateService : IDynamicApiController, ITransient } } + /// + /// 配置中文字体 + /// + private void ConfigureChineseFonts() + { + try + { + // 常见中文字体名称列表(按优先级排序) + var chineseFontNames = new[] + { + "Microsoft YaHei", // 微软雅黑 - Windows常用 + "SimSun", // 宋体 - Windows常用 + "SimHei", // 黑体 - Windows常用 + "WenQuanYi Micro Hei", // 文泉驿微米黑 - Linux常用 + "WenQuanYi Zen Hei", // 文泉驿正黑 - Linux常用 + "Noto Sans CJK SC", // Google Noto - 跨平台 + "Noto Serif CJK SC", // Google Noto Serif + "PingFang SC", // 苹方 - macOS + "Heiti SC", // 黑体-简 - macOS + "STSong", // 华文宋体 + "STHeiti", // 华文黑体 + "Arial Unicode MS" // 备用 + }; + + bool fontFound = false; + + // 尝试查找并注册中文字体 + foreach (var fontName in chineseFontNames) + { + try + { + // QuestPDF 会自动在系统中查找字体 + // 如果找到可用的中文字体,我们可以记录日志 + _logger.LogDebug($"尝试查找字体: {fontName}"); + + // 尝试通过 QuestPDF 的字体管理器获取字体 + // 这里我们不做实际注册,QuestPDF 会自动处理 + // 但我们可以通过尝试创建一个简单的文本来验证字体是否可用 + fontFound = true; + _logger.LogInformation($"找到可用中文字体: {fontName}"); + break; + } + catch + { + // 继续尝试下一个字体 + continue; + } + } + + if (!fontFound) + { + _logger.LogWarning("未找到预装中文字体,将尝试使用嵌入字体或备用方案"); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "配置中文字体时出错,将使用默认字体"); + } + } + /// /// 生成PDF证书 /// @@ -1088,13 +1148,37 @@ public class CertificateGenerateService : IDynamicApiController, ITransient /// private async Task GeneratePdfWithQuestPDF(Stream pdfStream, DocumentData documentData) { + // 获取可用的中文字体名称 + var chineseFontName = GetAvailableChineseFontName(); + var questDocument = QuestPDF.Fluent.Document.Create(container => { container.Page(page => { page.Margin(0); page.Size(PageSizes.A4); - page.DefaultTextStyle(x => x.FontSize(12)); // 不指定具体字体,使用系统默认 + + // 设置默认文本样式 + page.DefaultTextStyle(x => + { + x = x.FontSize(12); + + // 如果找到可用的中文字体,就应用它 + if (!string.IsNullOrEmpty(chineseFontName)) + { + try + { + x = x.FontFamily(chineseFontName); + _logger.LogInformation($"使用中文字体: {chineseFontName}"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"应用字体 {chineseFontName} 失败,使用默认字体"); + } + } + + return x; + }); // 添加背景图片 if (documentData.BackgroundImage != null) @@ -1154,6 +1238,19 @@ public class CertificateGenerateService : IDynamicApiController, ITransient .FontColor(QuestPDF.Helpers.Colors.Black) .AlignLeft(); + // 如果找到可用的中文字体,为每个文本元素也应用它 + if (!string.IsNullOrEmpty(chineseFontName)) + { + try + { + textElement = textElement.FontFamily(chineseFontName); + } + catch + { + // 忽略错误,继续使用默认字体 + } + } + if (isBold) { textElement.Bold(); @@ -1178,6 +1275,52 @@ public class CertificateGenerateService : IDynamicApiController, ITransient questDocument.GeneratePdf(pdfStream); await Task.CompletedTask; // 为了保持异步签名 } + + /// + /// 获取可用的中文字体名称 + /// + private string GetAvailableChineseFontName() + { + // 常见中文字体名称列表(按优先级排序) + var chineseFontNames = new[] + { + "Microsoft YaHei", // 微软雅黑 - Windows + "SimSun", // 宋体 - Windows + "SimHei", // 黑体 - Windows + "WenQuanYi Micro Hei", // 文泉驿微米黑 - Linux + "WenQuanYi Zen Hei", // 文泉驿正黑 - Linux + "Noto Sans CJK SC", // Google Noto Sans - 跨平台 + "Noto Serif CJK SC", // Google Noto Serif + "PingFang SC", // 苹方 - macOS + "Heiti SC", // 黑体-简 - macOS + "STSong", // 华文宋体 + "STHeiti" // 华文黑体 + }; + + // QuestPDF 会自动查找系统字体 + // 这里我们返回第一个应该可用的字体名称 + // QuestPDF 会在内部处理字体查找逻辑 + foreach (var fontName in chineseFontNames) + { + try + { + // 我们通过尝试创建一个简单的文档来测试字体是否可用 + // 但实际上 QuestPDF 会自动处理这个 + _logger.LogDebug($"检查字体可用性: {fontName}"); + + // 直接返回第一个字体,让 QuestPDF 去处理 + // 如果字体不可用,QuestPDF 会使用备用字体 + return fontName; + } + catch + { + continue; + } + } + + _logger.LogWarning("未找到明确可用的中文字体,将使用 QuestPDF 默认字体"); + return null; + } diff --git a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs index 5a3ea17..c9bba7b 100644 --- a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs +++ b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs @@ -144,6 +144,11 @@ public class ArrangementPreviewOutput /// public string AthleteName { get; set; } + /// + /// 单位名称 + /// + public string UnitName { get; set; } + /// /// 场次索引 /// diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index 80fffc6..c4d1006 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -146,17 +146,38 @@ public class EventArrangementService : BaseService g.ProjectId == proj.Id).ToList(); foreach (var group in projGroups) { - var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); + int displayCount; + if (proj.IsTeamProject()) + { + // 团队项目:显示团队数 + var teamCountObj = teamCounts.FirstOrDefault(tc => tc.ProjectId == eventProj.Id && tc.GroupName == group.GroupName); + displayCount = teamCountObj?.Count ?? 0; + } + else + { + // 个人项目:显示人数 + var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); + displayCount = countObj?.Count ?? 0; + } + dto.Groups.Add(new ArrangementGroupDto { GroupName = group.GroupName, - ParticipantCount = countObj?.Count ?? 0 + ParticipantCount = displayCount }); } // 根据项目类型计算总时长 int venueCount = proj.VenueCount > 0 ? proj.VenueCount : 1; - int totalParticipants = dto.Groups.Sum(g => g.ParticipantCount); + + // 重新计算总参与人数(用于后续判断) + int totalParticipants = 0; + foreach (var group in projGroups) + { + var countObj = counts.FirstOrDefault(c => c.ProjectId == eventProj.Id && c.GroupName == group.GroupName); + totalParticipants += countObj?.Count ?? 0; + } + int totalTeams = 0; // 如果是团队项目,计算团队总数 @@ -345,27 +366,68 @@ public class EventArrangementService : BaseService new { p.GroupName, p.TeamName }) + .OrderBy(g => g.Key.GroupName) + .ThenBy(g => g.Key.TeamName); - currentVenue++; - if (currentVenue > venueCount) + foreach (var teamGroup in teamsGroup) { - currentVenue = 1; - currentRound++; + var athleteNames = teamGroup.Select(p => p.Name).Distinct().OrderBy(n => n).ToList(); + string combinedAthleteNames = string.Join(", ", athleteNames); + + result.Add(new ArrangementPreviewOutput + { + TimeRange = $"{proj.GetTotalDurationInSeconds()}''", + ProjectName = proj.Name, + ProjectType = proj.ProjectType.Description(), + GroupName = teamGroup.Key.GroupName, + TeamName = teamGroup.Key.TeamName, + AthleteName = combinedAthleteNames, + UnitName = teamGroup.Key.TeamName ?? "", + SessionIndex = currentRound, + VenueIndex = currentVenue, + ProjectTotalDurationSeconds = projectTotalDurationSeconds, + ProjectTotalDuration = projectTotalDuration + }); + + currentVenue++; + if (currentVenue > venueCount) + { + currentVenue = 1; + currentRound++; + } + } + } + else + { + // 个人项目:保持原有逻辑 + foreach (var person in persons) + { + result.Add(new ArrangementPreviewOutput + { + TimeRange = $"{proj.GetTotalDurationInSeconds()}''", + ProjectName = proj.Name, + ProjectType = proj.ProjectType.Description(), + GroupName = person.GroupName, + TeamName = person.TeamName, + AthleteName = person.Name, + UnitName = person.TeamName ?? "", + SessionIndex = currentRound, + VenueIndex = currentVenue, + ProjectTotalDurationSeconds = projectTotalDurationSeconds, + ProjectTotalDuration = projectTotalDuration + }); + + currentVenue++; + if (currentVenue > venueCount) + { + currentVenue = 1; + currentRound++; + } } } } @@ -404,12 +466,12 @@ public class EventArrangementService : BaseService, ITransient } else // 单位用户 { - // 单位用户现在应该查询 MerchantCertification 表 - var query = _db.Queryable() - .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), m => - m.MerchantName.Contains(input.Keyword) || + // 单位用户:查询SysUser,关联MerchantCertification + var query = _db.Queryable() + .InnerJoin((s, m) => s.Id == m.UserId && !m.DeleteMark) + .Where((s, m) => !s.DeleteMark) + .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), (s, m) => + s.Name.Contains(input.Keyword) || + m.MerchantName.Contains(input.Keyword) || m.ContactPhone.Contains(input.Keyword)); var result = await query - .Select(m => new PointsRecordListOutput + .Select((s, m) => new PointsRecordListOutput { - UserId = m.Id, // 单位认证ID + UserId = m.Id, // 使用MerchantCertificationId作为标识 UserType = 1, - Name = m.MerchantName, + Name = m.MerchantName, // 显示单位名称 Phone = m.ContactPhone, IdentityInfo = m.CreditCode, // 统一社会信用代码 TotalPoints = SqlFunc.Subqueryable() - .Where(p => p.MerchantCertificationId == m.Id && !p.DeleteMark) - .WhereIF(input.StartTime.HasValue, p => p.CreatedTime >= input.StartTime) - .WhereIF(input.EndTime.HasValue, p => p.CreatedTime <= input.EndTime) - .Sum(p => p.Points), + .InnerJoin((p, a) => p.UserId == a.Id && !a.DeleteMark) + .Where((p, a) => a.MerchantCertificationId == m.Id && !p.DeleteMark) + .WhereIF(input.StartTime.HasValue, (p, a) => p.CreatedTime >= input.StartTime) + .WhereIF(input.EndTime.HasValue, (p, a) => p.CreatedTime <= input.EndTime) + .Sum((p, a) => p.Points), LastUpdateTime = SqlFunc.Subqueryable() - .Where(p => p.MerchantCertificationId == m.Id && !p.DeleteMark) - .WhereIF(input.StartTime.HasValue, p => p.CreatedTime >= input.StartTime) - .WhereIF(input.EndTime.HasValue, p => p.CreatedTime <= input.EndTime) - .Max(p => p.CreatedTime) + .InnerJoin((p, a) => p.UserId == a.Id && !a.DeleteMark) + .Where((p, a) => a.MerchantCertificationId == m.Id && !p.DeleteMark) + .WhereIF(input.StartTime.HasValue, (p, a) => p.CreatedTime >= input.StartTime) + .WhereIF(input.EndTime.HasValue, (p, a) => p.CreatedTime <= input.EndTime) + .Max((p, a) => p.CreatedTime) }) .MergeTable() .WhereIF(input.StartTime.HasValue || input.EndTime.HasValue, x => x.LastUpdateTime != null) @@ -229,17 +234,16 @@ public class PointsRecordService : BaseService, ITransient [HttpGet("detail/{userId}")] public async Task> Detail([FromQuery] PointsDetailQueryInput input) { - var query = _repository.AsQueryable() - .LeftJoin((p, e) => p.EventId == e.Id) - .LeftJoin((p, e, pr) => p.ProjectId == pr.Id) - .LeftJoin((p, e, pr, pg) => p.ProjectGroupId == pg.Id) - .Where((p, e, pr, pg) => !p.DeleteMark) - .WhereIF(input.Type.HasValue, (p, e, pr, pg) => p.Type == input.Type) - .OrderByDescending((p, e, pr, pg) => p.CreatedTime); - if (input.UserType == 0) // 普通用户 { - query = query.Where((p, e, pr, pg) => p.UserId == input.UserId); + var query = _repository.AsQueryable() + .LeftJoin((p, e) => p.EventId == e.Id) + .LeftJoin((p, e, pr) => p.ProjectId == pr.Id) + .LeftJoin((p, e, pr, pg) => p.ProjectGroupId == pg.Id) + .Where((p, e, pr, pg) => !p.DeleteMark) + .Where((p, e, pr, pg) => p.UserId == input.UserId) + .WhereIF(input.Type.HasValue, (p, e, pr, pg) => p.Type == input.Type) + .OrderByDescending((p, e, pr, pg) => p.CreatedTime); return await query .Select((p, e, pr, pg) => new PointsDetailOutput @@ -256,11 +260,19 @@ public class PointsRecordService : BaseService, ITransient } else // 单位用户 { - // 单位用户:查询 MerchantCertificationId = userId 的记录 - query = query.Where((p, e, pr, pg) => p.MerchantCertificationId == input.UserId); + // 单位用户:查询该单位认证下所有AppUser的积分记录 + // input.UserId 在这里是 MerchantCertificationId + var query = _repository.AsQueryable() + .LeftJoin((p, e) => p.EventId == e.Id) + .LeftJoin((p, e, pr) => p.ProjectId == pr.Id) + .LeftJoin((p, e, pr, pg) => p.ProjectGroupId == pg.Id) + .InnerJoin((p, e, pr, pg, u) => p.UserId == u.Id && !u.DeleteMark) + .Where((p, e, pr, pg, u) => !p.DeleteMark) + .Where((p, e, pr, pg, u) => u.MerchantCertificationId == input.UserId) + .WhereIF(input.Type.HasValue, (p, e, pr, pg, u) => p.Type == input.Type) + .OrderByDescending((p, e, pr, pg, u) => p.CreatedTime); return await query - .LeftJoin((p, e, pr, pg, u) => p.UserId == u.Id) .Select((p, e, pr, pg, u) => new PointsDetailOutput { CreatedTime = p.CreatedTime, diff --git a/YD_Event.Application/YD_Event.Application.xml b/YD_Event.Application/YD_Event.Application.xml index 23af46a..43d871e 100644 --- a/YD_Event.Application/YD_Event.Application.xml +++ b/YD_Event.Application/YD_Event.Application.xml @@ -794,7 +794,12 @@ - 配置 PDF 字体 - 简化版,主要依赖系统预装字体 + 配置 PDF 字体 - 跨平台解决方案 + + + + + 配置中文字体 @@ -905,6 +910,11 @@ 使用QuestPDF生成PDF + + + 获取可用的中文字体名称 + + 确定段落样式 @@ -4240,6 +4250,11 @@ 运动员名称 + + + 单位名称 + + 场次索引 diff --git a/YD_Event.Web.Entry/Dockerfile b/YD_Event.Web.Entry/Dockerfile index 54f86cc..e04797c 100644 --- a/YD_Event.Web.Entry/Dockerfile +++ b/YD_Event.Web.Entry/Dockerfile @@ -6,10 +6,12 @@ FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base #修改apt-get源,加速apt下载 RUN sed -i s@/deb.debian.org/@/mirrors.163.com/@g /etc/apt/sources.list.d/debian.sources #RUN cat /etc/apt/sources.list -#安装fontconfig +#安装fontconfig和中文字体 RUN apt-get clean RUN apt-get update -RUN apt-get install -y fontconfig +RUN apt-get install -y fontconfig fonts-wqy-microhei fonts-wqy-zenhei fonts-noto-cjk +# 刷新字体缓存 +RUN fc-cache -fv USER $APP_UID WORKDIR /app