积分显示

This commit is contained in:
马梓彭 2026-04-21 11:43:49 +08:00
parent 1848a46398
commit 56cb66e4ad
6 changed files with 303 additions and 63 deletions

View File

@ -71,7 +71,7 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
}
/// <summary>
/// 配置 PDF 字体 - 简化版,主要依赖系统预装字体
/// 配置 PDF 字体 - 跨平台解决方案
/// </summary>
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
}
}
/// <summary>
/// 配置中文字体
/// </summary>
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, "配置中文字体时出错,将使用默认字体");
}
}
/// <summary>
/// 生成PDF证书
/// </summary>
@ -1088,13 +1148,37 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
/// </summary>
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; // 为了保持异步签名
}
/// <summary>
/// 获取可用的中文字体名称
/// </summary>
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;
}

View File

@ -144,6 +144,11 @@ public class ArrangementPreviewOutput
/// </summary>
public string AthleteName { get; set; }
/// <summary>
/// 单位名称
/// </summary>
public string UnitName { get; set; }
/// <summary>
/// 场次索引
/// </summary>

View File

@ -146,17 +146,38 @@ public class EventArrangementService : BaseService<YD_Event.Core.Entities.Projec
var projGroups = groups.Where(g => 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<YD_Event.Core.Entities.Projec
int currentRound = 1;
int currentVenue = 1;
foreach (var person in persons)
if (proj.IsTeamProject())
{
result.Add(new ArrangementPreviewOutput
{
TimeRange = $"{proj.GetTotalDurationInSeconds()}''",
ProjectName = proj.Name,
ProjectType = proj.ProjectType.Description(),
GroupName = person.GroupName,
TeamName = person.TeamName,
AthleteName = person.Name,
SessionIndex = currentRound,
VenueIndex = currentVenue,
ProjectTotalDurationSeconds = projectTotalDurationSeconds,
ProjectTotalDuration = projectTotalDuration
});
// 团队项目:按团队分组,同一团队的运动员姓名用逗号分隔
var teamsGroup = persons
.GroupBy(p => 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<YD_Event.Core.Entities.Projec
{
CreateWorksheet("编排表");
// 第一行:赛事标题(合并单元格 A1:G1)
// 第一行:赛事标题(合并单元格 A1:H1)
var titleColor = Color.FromArgb(255, 217, 179);
SetMergeTitle(_eventInfo?.EventName ?? "赛事编排表", 1, 1, 1, 7, titleColor, 14);
SetMergeTitle(_eventInfo?.EventName ?? "赛事编排表", 1, 1, 1, 8, titleColor, 14);
// 第二行:列标题
var headers = new[] { "时间", "参赛项目", "参赛组别", "参赛队伍", "运动员姓名", "场次", "场地" };
var headers = new[] { "时间", "参赛项目", "参赛组别", "参赛队伍", "运动员姓名", "单位", "场次", "场地" };
var headerColor = Color.FromArgb(204, 255, 204);
SetHeaders(headers, 2, headerColor, 11);
@ -460,10 +522,11 @@ public class EventArrangementService : BaseService<YD_Event.Core.Entities.Projec
Worksheet.Cells[rowIndex, 3].Value = item.GroupName;
Worksheet.Cells[rowIndex, 4].Value = item.TeamName ?? "";
Worksheet.Cells[rowIndex, 5].Value = item.AthleteName;
Worksheet.Cells[rowIndex, 6].Value = item.SessionIndex;
Worksheet.Cells[rowIndex, 7].Value = item.VenueIndex;
Worksheet.Cells[rowIndex, 6].Value = item.UnitName;
Worksheet.Cells[rowIndex, 7].Value = item.SessionIndex;
Worksheet.Cells[rowIndex, 8].Value = item.VenueIndex;
SetDataRowStyle(rowIndex, 7, alternateBgColor, i % 2 == 1);
SetDataRowStyle(rowIndex, 8, alternateBgColor, i % 2 == 1);
}
// 处理最后一个项目的合并
@ -484,7 +547,7 @@ public class EventArrangementService : BaseService<YD_Event.Core.Entities.Projec
}
// 设置列宽
var widths = new double[] { 12, 18, 15, 25, 15, 8, 8 };
var widths = new double[] { 12, 18, 15, 25, 15, 20, 8, 8 };
SetColumnWidths(widths);
// 设置行高

View File

@ -187,30 +187,35 @@ public class PointsRecordService : BaseService<PointsRecord>, ITransient
}
else // 单位用户
{
// 单位用户现在应该查询 MerchantCertification 表
var query = _db.Queryable<MerchantCertification>()
.WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), m =>
m.MerchantName.Contains(input.Keyword) ||
// 单位用户:查询SysUser,关联MerchantCertification
var query = _db.Queryable<SysUser>()
.InnerJoin<MerchantCertification>((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<PointsRecord>()
.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<YD_Event.Core.Entities.AppUser>((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<PointsRecord>()
.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<YD_Event.Core.Entities.AppUser>((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<PointsRecord>, ITransient
[HttpGet("detail/{userId}")]
public async Task<PageResult<PointsDetailOutput>> Detail([FromQuery] PointsDetailQueryInput input)
{
var query = _repository.AsQueryable()
.LeftJoin<EventInfo>((p, e) => p.EventId == e.Id)
.LeftJoin<YD_Event.Core.Entities.Project>((p, e, pr) => p.ProjectId == pr.Id)
.LeftJoin<ProjectGroup>((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<EventInfo>((p, e) => p.EventId == e.Id)
.LeftJoin<YD_Event.Core.Entities.Project>((p, e, pr) => p.ProjectId == pr.Id)
.LeftJoin<ProjectGroup>((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<PointsRecord>, ITransient
}
else // 单位用户
{
// 单位用户:查询 MerchantCertificationId = userId 的记录
query = query.Where((p, e, pr, pg) => p.MerchantCertificationId == input.UserId);
// 单位用户:查询该单位认证下所有AppUser的积分记录
// input.UserId 在这里是 MerchantCertificationId
var query = _repository.AsQueryable()
.LeftJoin<EventInfo>((p, e) => p.EventId == e.Id)
.LeftJoin<YD_Event.Core.Entities.Project>((p, e, pr) => p.ProjectId == pr.Id)
.LeftJoin<ProjectGroup>((p, e, pr, pg) => p.ProjectGroupId == pg.Id)
.InnerJoin<YD_Event.Core.Entities.AppUser>((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<YD_Event.Core.Entities.AppUser>((p, e, pr, pg, u) => p.UserId == u.Id)
.Select((p, e, pr, pg, u) => new PointsDetailOutput
{
CreatedTime = p.CreatedTime,

View File

@ -794,7 +794,12 @@
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ConfigurePdfFonts">
<summary>
配置 PDF 字体 - 简化版,主要依赖系统预装字体
配置 PDF 字体 - 跨平台解决方案
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.ConfigureChineseFonts">
<summary>
配置中文字体
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GeneratePdf(YD_Event.Application.Certificate.Dtos.GeneratePdfCertificateInput)">
@ -905,6 +910,11 @@
使用QuestPDF生成PDF
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.GetAvailableChineseFontName">
<summary>
获取可用的中文字体名称
</summary>
</member>
<member name="M:YD_Event.Application.Certificate.CertificateGenerateService.DetermineParagraphStyle(DocumentFormat.OpenXml.Wordprocessing.Paragraph)">
<summary>
确定段落样式
@ -4240,6 +4250,11 @@
运动员名称
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.ArrangementPreviewOutput.UnitName">
<summary>
单位名称
</summary>
</member>
<member name="P:YD_Event.Application.Event.Dtos.ArrangementPreviewOutput.SessionIndex">
<summary>
场次索引

View File

@ -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