using YD_Event.Application.Certificate.Dtos;
using YD_Event.Core.Entities;
using YD_Event.Core.Domain.Certificate;
using SqlSugar;
using Mapster;
using Microsoft.Extensions.Options;
using YD_Event.Core.Options;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.ComponentModel.DataAnnotations;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using System.IO;
using System.Linq;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using YD_Event.Application.File;
namespace YD_Event.Application.Certificate;
///
/// 证书生成服务
///
public class CertificateGenerateService : IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository _certificateRepository;
private readonly ISqlSugarRepository _templateRepository;
private readonly ISqlSugarRepository _placeholderRepository;
private readonly FileService _fileService;
private readonly IOptionsMonitor _ossOptionsMonitor;
private readonly IIdGenerator _idGenerator;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger _logger;
private readonly DocxTemplateProcessor _docxProcessor;
private readonly QRCodeService _qrCodeService;
public CertificateGenerateService(
ISqlSugarRepository certificateRepository,
ISqlSugarRepository templateRepository,
ISqlSugarRepository placeholderRepository,
FileService fileService,
IOptionsMonitor ossOptionsMonitor,
IIdGenerator idGenerator,
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor,
ILogger logger,
DocxTemplateProcessor docxProcessor,
QRCodeService qrCodeService)
{
_certificateRepository = certificateRepository;
_templateRepository = templateRepository;
_placeholderRepository = placeholderRepository;
_fileService = fileService;
_ossOptionsMonitor = ossOptionsMonitor;
_idGenerator = idGenerator;
_webHostEnvironment = webHostEnvironment;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_docxProcessor = docxProcessor;
_qrCodeService = qrCodeService;
}
///
/// 生成PDF证书
///
[DisplayName("生成PDF证书")]
public async Task GeneratePdf(GeneratePdfCertificateInput input)
{
var stopwatch = Stopwatch.StartNew();
var result = new PdfCertificateGenerationResult
{
GeneratedTime = DateTime.Now,
IsSuccess = true
};
try
{
// 1. 获取证书信息
var certificate = await _certificateRepository.GetByIdAsync(input.CertificateId);
if (certificate == null)
{
result.IsSuccess = false;
result.ErrorMessage = "证书不存在";
return result;
}
if (certificate.Status != Core.Enum.AvailabilityStatus.Enable)
{
result.IsSuccess = false;
result.ErrorMessage = "证书已禁用";
return result;
}
// 2. 获取模板
CertificateTemplate template;
if (input.TemplateId.HasValue)
{
template = await _templateRepository.GetByIdAsync(input.TemplateId.Value);
}
else
{
template = await _templateRepository.GetByIdAsync(certificate.TemplateId);
}
if (template == null)
{
result.IsSuccess = false;
result.ErrorMessage = "未找到有效证书模板";
return result;
}
// 3. 检查模板类型
if (template.TemplateType != "Doc")
{
result.IsSuccess = false;
result.ErrorMessage = "指定的模板不是DOC类型";
return result;
}
// 4. 获取占位符配置
var placeholders = await _placeholderRepository.AsQueryable()
.Where(x => x.TemplateId == template.Id)
.OrderBy(x => x.Sort)
.ToListAsync();
// 5. 验证必填数据
var requiredPlaceholders = placeholders.Where(x => x.IsRequired).Select(x => x.PlaceholderKey).ToList();
var validationResult = _docxProcessor.ValidateTemplateData(requiredPlaceholders, input.Data);
if (!validationResult.IsValid)
{
result.IsSuccess = false;
result.ErrorMessage = $"缺少必填数据: {string.Join(", ", validationResult.MissingPlaceholders)}";
return result;
}
// 6. 下载DOC模板
using var httpClient = new HttpClient();
var templateStream = new MemoryStream(await httpClient.GetByteArrayAsync(template.DocxTemplateUrl));
// 7. 生成二维码数据(用于DOC模板中的占位符)
var qrCodeData = new Dictionary();
if (input.GenerateQRCode)
{
var certificateUrl = GenerateCertificateUrl(input.CertificateId);
qrCodeData["CertificateQR"] = certificateUrl;
qrCodeData["VerificationQR"] = certificateUrl + "/verify";
}
// 8. 处理模板
var processedStream = _docxProcessor.ProcessTemplate(templateStream, input.Data, qrCodeData);
// 9. 先生成证书保存地址(预设的PDF地址)
var pdfUrl = GeneratePdfUrl(input.CertificateId, certificate, template);
// 10. 生成二维码图片(使用预设的PDF地址)
byte[] qrCodeImage = null;
if (input.GenerateQRCode)
{
using var qrStream = _qrCodeService.GenerateQRCode(pdfUrl, input.QRCodeSize);
using var memoryStream = new MemoryStream();
qrStream.CopyTo(memoryStream);
qrCodeImage = memoryStream.ToArray();
}
// 11. 转换为PDF(包含二维码)
var pdfStream = await ConvertDocToPdf(processedStream, qrCodeImage);
// 12. 保存PDF文件到预设地址
await SavePdfFileToUrl(pdfStream, pdfUrl);
// 12. 生成二维码图片(如果需要)
string qrCodeUrl = null;
if (input.GenerateQRCode)
{
qrCodeUrl = await GenerateQRCodeImage(input.CertificateId, input.QRCodeSize);
}
// 13. 设置结果
result.PdfUrl = pdfUrl;
result.QRCodeUrl = qrCodeUrl;
result.VerificationUrl = GenerateCertificateUrl(input.CertificateId);
result.Warnings.AddRange(validationResult.Warnings);
_logger.LogInformation("PDF证书生成成功. CertificateId: {CertificateId}, Url: {Url}",
input.CertificateId, pdfUrl);
}
catch (Exception ex)
{
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
_logger.LogError(ex, "PDF证书生成失败. CertificateId: {CertificateId}", input.CertificateId);
}
finally
{
stopwatch.Stop();
result.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
}
return result;
}
///
/// 批量生成PDF证书
///
[DisplayName("批量生成PDF证书")]
public async Task BatchGeneratePdf(BatchGeneratePdfCertificateInput input)
{
var result = new BatchPdfCertificateGenerationResult
{
StartTime = DateTime.Now,
TotalCount = input.DataList.Count
};
_logger.LogInformation("开始批量生成PDF证书. Count: {Count}", input.DataList.Count);
foreach (var data in input.DataList)
{
var generateInput = new GeneratePdfCertificateInput
{
CertificateId = input.CertificateId,
TemplateId = input.TemplateId,
Data = data,
GenerateQRCode = input.GenerateQRCode,
QRCodeSize = input.QRCodeSize
};
var generateResult = await GeneratePdf(generateInput);
if (generateResult.IsSuccess)
{
result.SuccessResults.Add(generateResult);
}
else
{
result.FailedResults.Add(generateResult);
}
}
result.EndTime = DateTime.Now;
_logger.LogInformation("批量生成PDF证书完成. Total: {Total}, Success: {Success}, Failed: {Failed}",
result.TotalCount, result.SuccessCount, result.FailedCount);
return result;
}
///
/// 转换DOC为PDF(使用QuestPDF和DocumentFormat.OpenXml)
///
private async Task ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = null)
{
try
{
_logger.LogInformation("开始DOCX转PDF转换");
// 重置流位置
docStream.Position = 0;
// 使用QuestPDF许可证
QuestPDF.Settings.License = LicenseType.Community;
// 解析DOCX内容
var documentData = await ExtractDocumentData(docStream);
// 添加二维码图片
if (qrCodeImage != null)
{
documentData.QRCodeImage = qrCodeImage;
documentData.QRCodeMimeType = "image/png";
}
// 生成PDF
var pdfStream = new MemoryStream();
await GeneratePdfWithQuestPDF(pdfStream, documentData);
pdfStream.Position = 0;
_logger.LogInformation("DOCX转PDF完成,PDF大小: {Size} bytes", pdfStream.Length);
return pdfStream;
}
catch (Exception ex)
{
_logger.LogError(ex, "DOCX转PDF失败");
throw new Exception("DOCX转PDF转换失败", ex);
}
}
///
/// 提取DOCX文档数据(包括段落、表格、背景图片等)
///
private async Task ExtractDocumentData(Stream docStream)
{
var documentData = new DocumentData();
using var wordprocessingDocument = WordprocessingDocument.Open(docStream, false);
var body = wordprocessingDocument.MainDocumentPart?.Document?.Body;
// 提取背景图片
await ExtractBackgroundImage(wordprocessingDocument, documentData);
if (body != null)
{
// 提取段落(包含位置信息)
foreach (var paragraph in body.Elements())
{
var paragraphText = paragraph.InnerText?.Trim();
if (!string.IsNullOrEmpty(paragraphText))
{
var paragraphData = new ParagraphData
{
Text = paragraphText,
Style = DetermineParagraphStyle(paragraph),
FontSize = GetParagraphFontSize(paragraph),
Alignment = GetParagraphAlignment(paragraph),
IsBold = IsParagraphBold(paragraph),
FontColor = GetParagraphColor(paragraph)
};
// 尝试获取位置信息
GetParagraphPosition(paragraph, paragraphData);
documentData.Paragraphs.Add(paragraphData);
}
}
// 提取表格
foreach (var table in body.Elements())
{
var tableData = new TableData();
foreach (var row in table.Elements())
{
var rowData = new List();
foreach (var cell in row.Elements())
{
var cellText = cell.InnerText?.Trim() ?? string.Empty;
rowData.Add(cellText);
}
if (rowData.Any())
{
tableData.Rows.Add(rowData);
}
}
if (tableData.Rows.Any())
{
documentData.Tables.Add(tableData);
}
}
}
return documentData;
}
///
/// 提取背景图片
///
private async Task ExtractBackgroundImage(WordprocessingDocument wordDoc, DocumentData documentData)
{
try
{
var mainPart = wordDoc.MainDocumentPart;
if (mainPart != null)
{
// 查找文档中的图片
var imageParts = mainPart.GetPartsOfType().ToList();
if (imageParts.Any())
{
// 尝试获取第一个图片作为背景图片
var firstImage = imageParts.FirstOrDefault();
if (firstImage != null)
{
using var imageStream = firstImage.GetStream(FileMode.Open, FileAccess.Read);
using var memoryStream = new MemoryStream();
await imageStream.CopyToAsync(memoryStream);
documentData.BackgroundImage = memoryStream.ToArray();
documentData.BackgroundImageMimeType = firstImage.ContentType;
_logger.LogInformation("成功提取背景图片,类型: {Type}, 大小: {Size} bytes",
documentData.BackgroundImageMimeType, documentData.BackgroundImage.Length);
}
}
// 如果没有找到图片,尝试从页眉页脚中查找
if (documentData.BackgroundImage == null)
{
var headerParts = mainPart.HeaderParts;
foreach (var headerPart in headerParts)
{
var headerImages = headerPart.GetPartsOfType();
foreach (var image in headerImages)
{
using var imageStream = image.GetStream(FileMode.Open, FileAccess.Read);
using var memoryStream = new MemoryStream();
await imageStream.CopyToAsync(memoryStream);
documentData.BackgroundImage = memoryStream.ToArray();
documentData.BackgroundImageMimeType = image.ContentType;
_logger.LogInformation("从页眉中提取背景图片,类型: {Type}, 大小: {Size} bytes",
documentData.BackgroundImageMimeType, documentData.BackgroundImage.Length);
break;
}
if (documentData.BackgroundImage != null) break;
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "提取背景图片失败");
}
}
///
/// 获取段落字体大小
///
private double? GetParagraphFontSize(Paragraph paragraph)
{
var runProperties = paragraph.Elements()
.SelectMany(r => r.Elements())
.FirstOrDefault();
if (runProperties?.FontSize != null)
{
var fontSize = runProperties.FontSize.Val;
if (double.TryParse(fontSize, out double size))
{
return size / 2.0; // OpenXML 中字体大小是半磅为单位
}
}
return null;
}
///
/// 获取段落对齐方式
///
private string GetParagraphAlignment(Paragraph paragraph)
{
var paragraphProperties = paragraph.ParagraphProperties;
if (paragraphProperties?.Justification != null)
{
var justification = paragraphProperties.Justification.Val?.ToString().ToLower();
return justification switch
{
"center" => "center",
"right" => "right",
"both" => "justify",
_ => "left"
};
}
return "left";
}
///
/// 检查段落是否为粗体
///
private bool IsParagraphBold(Paragraph paragraph)
{
var runProperties = paragraph.Elements()
.SelectMany(r => r.Elements())
.FirstOrDefault();
return runProperties?.Bold != null;
}
///
/// 获取段落字体颜色
///
private string GetParagraphColor(Paragraph paragraph)
{
var runProperties = paragraph.Elements()
.SelectMany(r => r.Elements())
.FirstOrDefault();
if (runProperties?.Color != null)
{
var color = runProperties.Color.Val;
if (!string.IsNullOrEmpty(color))
{
return color;
}
}
return "black";
}
///
/// 获取段落位置信息
///
private void GetParagraphPosition(Paragraph paragraph, ParagraphData paragraphData)
{
try
{
var paragraphProperties = paragraph.ParagraphProperties;
if (paragraphProperties != null)
{
_logger.LogDebug("处理段落: {Text}", paragraphData.Text);
// 检查是否有绝对定位
var frameProperties = paragraphProperties.Elements().FirstOrDefault();
if (frameProperties != null)
{
_logger.LogDebug("找到FrameProperties");
// 提取位置信息
if (frameProperties.Width != null)
{
var widthStr = frameProperties.Width.Value.ToString();
_logger.LogDebug("Width: {Width}", widthStr);
if (uint.TryParse(widthStr, out uint width))
{
paragraphData.PositionX = width;
_logger.LogDebug("设置PositionX: {X}", width);
}
}
if (frameProperties.Height != null)
{
var heightStr = frameProperties.Height.Value.ToString();
_logger.LogDebug("Height: {Height}", heightStr);
if (uint.TryParse(heightStr, out uint height))
{
paragraphData.PositionY = height;
_logger.LogDebug("设置PositionY: {Y}", height);
}
}
}
else
{
_logger.LogDebug("未找到FrameProperties");
}
// 如果没有绝对定位,检查段落缩进
if (paragraphData.PositionX == null)
{
var indentation = paragraphProperties.Elements().FirstOrDefault();
if (indentation != null && indentation.Left != null)
{
var leftStr = indentation.Left.Value.ToString();
_logger.LogDebug("Left Indentation: {Indent}", leftStr);
if (uint.TryParse(leftStr, out uint leftIndent))
{
paragraphData.PositionX = leftIndent / 20; // 转换为点
_logger.LogDebug("通过缩进设置PositionX: {X}", paragraphData.PositionX);
}
}
}
// 检查段落间距
var spacingBetweenLines = paragraphProperties.SpacingBetweenLines;
if (spacingBetweenLines != null)
{
_logger.LogDebug("SpacingBetweenLines: {Spacing}", spacingBetweenLines.ToString());
}
}
_logger.LogDebug("最终位置 - X: {X}, Y: {Y}", paragraphData.PositionX, paragraphData.PositionY);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取段落位置信息失败");
}
}
///
/// 使用QuestPDF生成PDF
///
private async Task GeneratePdfWithQuestPDF(Stream pdfStream, DocumentData documentData)
{
var questDocument = QuestPDF.Fluent.Document.Create(container =>
{
container.Page(page =>
{
page.Margin(0);
page.Size(PageSizes.A4);
page.DefaultTextStyle(x => x.FontSize(12).FontFamily(QuestPDF.Helpers.Fonts.Calibri));
// 添加背景图片
if (documentData.BackgroundImage != null)
{
page.Background()
.Image(documentData.BackgroundImage)
.FitArea();
}
// 使用相对定位来放置文本
page.Content().Column(column =>
{
// 根据段落信息进行精确定位
foreach (var paragraph in documentData.Paragraphs)
{
var fontSize = paragraph.FontSize ?? 12;
var isBold = paragraph.IsBold;
// 使用从DOCX中提取的实际位置信息
var positionX = paragraph.PositionX ?? 50; // 默认左边距
var positionY = paragraph.PositionY ?? 50; // 默认顶部边距
// 跳过无效的文本内容
if (string.IsNullOrWhiteSpace(paragraph.Text) ||
paragraph.Text.Contains("left-") ||
paragraph.Text.Contains("right-") ||
paragraph.Text.Contains("top-") ||
paragraph.Text.Contains("bottom-"))
{
continue;
}
// 创建文本元素,使用原始位置
var textElement = column.Item().PaddingTop((float)positionY).PaddingLeft((float)positionX).Text(paragraph.Text)
.FontSize((float)fontSize)
.FontColor(QuestPDF.Helpers.Colors.Black);
// 如果需要粗体,应用到同一个元素
if (isBold)
{
textElement.Bold();
}
}
// 添加二维码到右下角
if (documentData.QRCodeImage != null)
{
column.Item().PaddingTop(500).PaddingRight(50).AlignRight().Width(80).Height(80).Image(documentData.QRCodeImage);
}
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("生成时间: ").FontSize(8).FontColor(QuestPDF.Helpers.Colors.Grey.Darken1);
x.CurrentPageNumber().FontSize(8).FontColor(QuestPDF.Helpers.Colors.Grey.Darken1);
});
});
});
// 生成PDF到流
questDocument.GeneratePdf(pdfStream);
await Task.CompletedTask; // 为了保持异步签名
}
///
/// 根据段落内容确定文本位置(基于A4页面210x297mm,坐标单位为点)
///
private (double X, double Y) GetTextPosition(ParagraphData paragraph)
{
// 根据文本内容和样式判断位置
var text = paragraph.Text;
// 根据证书模板的布局进行定位(A4页面,左上角为原点)
// 调整坐标以更好地匹配证书模板
if (text.Contains("第九届上海国际交互绳大奖赛"))
{
return (150, 80); // 顶部标题位置(居中)
}
else if (text.Contains("Double Dutch Contest Shanghai"))
{
return (150, 120); // 英文标题位置(居中)
}
else if (text.Contains("CERTIFICATE"))
{
return (250, 200); // 证书标题位置(居中)
}
else if (text.Contains("比赛证书"))
{
return (250, 240); // 中文证书标题位置(居中)
}
else if (text.Contains("参赛单位"))
{
return (100, 320); // 参赛单位标签位置
}
else if (text.Contains("{{danwei}}"))
{
return (200, 320); // 参赛单位值位置
}
else if (text.Contains("参赛运动员"))
{
return (100, 370); // 参赛运动员标签位置
}
else if (text.Contains("{{name}}"))
{
return (200, 370); // 参赛运动员值位置
}
else if (text.Contains("项目组别"))
{
return (100, 420); // 项目组别标签位置
}
else if (text.Contains("名次"))
{
return (100, 470); // 名次标签位置
}
else if (text.Contains("第九届上海国际交互绳大奖赛组委会"))
{
return (150, 550); // 底部组织机构位置(居中)
}
else if (text.Contains("二零二五年十一月"))
{
return (250, 590); // 日期位置(居中)
}
// 默认位置
return (50, 50);
}
///
/// 确定段落样式
///
private string DetermineParagraphStyle(Paragraph paragraph)
{
// 检查是否为标题
var paragraphProperties = paragraph.ParagraphProperties;
if (paragraphProperties != null)
{
var style = paragraphProperties.ParagraphStyleId?.Val?.Value?.ToLower();
if (style != null)
{
if (style.Contains("heading") || style.Contains("title"))
{
return "heading";
}
}
}
// 根据文本特征判断
var text = paragraph.InnerText?.Trim() ?? string.Empty;
if (text.Length < 50 && text.Length > 0)
{
return "subheading";
}
return "normal";
}
///
/// 获取LibreOffice可执行文件路径
///
private string GetLibreOfficePath()
{
var possiblePaths = new[]
{
// Windows
@"C:\Program Files\LibreOffice\program\soffice.exe",
@"C:\Program Files (x86)\LibreOffice\program\soffice.exe",
@"C:\Program Files\LibreOffice\program\libreoffice.exe",
@"C:\Program Files (x86)\LibreOffice\program\libreoffice.exe",
// Linux
"/usr/bin/libreoffice",
"/usr/bin/soffice",
"/opt/libreoffice/program/soffice",
// macOS
"/Applications/LibreOffice.app/Contents/MacOS/soffice"
};
foreach (var path in possiblePaths)
{
if (System.IO.File.Exists(path))
{
_logger.LogInformation("找到LibreOffice: {Path}", path);
return path;
}
}
// 如果找不到,尝试使用系统PATH中的命令
return OperatingSystem.IsWindows() ? "soffice" : "libreoffice";
}
///
/// 保存PDF文件
///
private async Task SavePdfFile(Stream pdfStream, YD_Event.Core.Entities.Certificate certificate, CertificateTemplate template)
{
var options = _ossOptionsMonitor.CurrentValue;
var now = DateTime.Today;
string name = $"certificate_{_idGenerator.Encode(_idGenerator.NewLong())}.pdf";
string filePath = $"/certificates/{now.Year}/{now.Month:D2}/{now.Day:D2}/";
string finalUrl = string.Empty;
pdfStream.Position = 0;
if (!options.Enable)
{
// 本地存储
string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath);
string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/'));
if (!Directory.Exists(absoluteDir))
{
Directory.CreateDirectory(absoluteDir);
}
string fullPath = Path.Combine(absoluteDir, name);
// 预测最终 URL
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}";
}
using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
await pdfStream.CopyToAsync(fileStream);
_logger.LogInformation("PDF文件保存到本地: {FullPath}", fullPath);
}
else
{
// OSS 上传
string fileName = $"{filePath.TrimStart('/')}{name}";
finalUrl = await _fileService.UploadFileStreamAsync(fileName, pdfStream);
_logger.LogInformation("PDF文件上传到OSS: {FileName}", fileName);
}
return finalUrl;
}
///
/// 生成二维码图片
///
private async Task GenerateQRCodeImage(long certificateId, int size)
{
try
{
var certificateUrl = GenerateCertificateUrl(certificateId);
using var qrStream = _qrCodeService.GenerateQRCode(certificateUrl, size);
return await SaveQRCodeImage(qrStream, certificateId, size);
}
catch (Exception ex)
{
_logger.LogError(ex, "生成二维码图片失败. CertificateId: {CertificateId}", certificateId);
return null;
}
}
///
/// 保存二维码图片
///
private async Task SaveQRCodeImage(Stream qrStream, long certificateId, int size)
{
var options = _ossOptionsMonitor.CurrentValue;
var now = DateTime.Today;
string name = $"qr_{certificateId}_{_idGenerator.Encode(_idGenerator.NewLong())}";
string filePath = $"/{now.Year}/{now.Month:D2}/{now.Day:D2}/qrcodes/";
string extension = ".png";
string finalUrl = string.Empty;
qrStream.Position = 0;
if (!options.Enable)
{
string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath);
string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/'));
if (!Directory.Exists(absoluteDir))
{
Directory.CreateDirectory(absoluteDir);
}
string fullPath = Path.Combine(absoluteDir, $"{name}{extension}");
using var fileStream2 = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
await qrStream.CopyToAsync(fileStream2);
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}{extension}";
}
}
else
{
// OSS 上传
string fileName = $"{filePath.TrimStart('/')}{name}{extension}";
finalUrl = await _fileService.UploadFileStreamAsync(fileName, qrStream);
}
return finalUrl;
}
///
/// 生成证书URL
///
private string GenerateCertificateUrl(long certificateId)
{
var request = _httpContextAccessor.HttpContext?.Request;
if (request == null) return string.Empty;
var baseUrl = $"{request.Scheme}://{request.Host.Value}";
return _qrCodeService.GenerateCertificateUrl(certificateId, baseUrl);
}
///
/// 生成PDF保存地址
///
private string GeneratePdfUrl(long certificateId, YD_Event.Core.Entities.Certificate certificate, CertificateTemplate template)
{
var options = _ossOptionsMonitor.CurrentValue;
var now = DateTime.Today;
string name = $"certificate_{_idGenerator.Encode(_idGenerator.NewLong())}.pdf";
string filePath = $"/certificates/{now.Year}/{now.Month:D2}/{now.Day:D2}/";
string finalUrl = string.Empty;
if (!options.Enable)
{
// 本地存储
string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath);
// 预测最终 URL
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}";
}
}
else
{
// OSS 上传
string fileName = $"{filePath.TrimStart('/')}{name}";
finalUrl = $"{options.Domain.TrimEnd('/')}/{fileName}";
}
return finalUrl;
}
///
/// 保存PDF文件到指定URL
///
private async Task SavePdfFileToUrl(Stream pdfStream, string targetUrl)
{
var options = _ossOptionsMonitor.CurrentValue;
// 从URL中解析文件路径
var uri = new Uri(targetUrl);
var path = uri.AbsolutePath;
// 移除域名部分,获取相对路径
string relativePath = path;
if (path.StartsWith("/"))
{
relativePath = path.Substring(1);
}
pdfStream.Position = 0;
if (!options.Enable)
{
// 本地存储
string localDir = Path.GetDirectoryName(relativePath);
string fileName = Path.GetFileName(relativePath);
string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir);
if (!Directory.Exists(absoluteDir))
{
Directory.CreateDirectory(absoluteDir);
}
string fullPath = Path.Combine(absoluteDir, fileName);
using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
await pdfStream.CopyToAsync(fileStream);
_logger.LogInformation("PDF文件保存到本地: {FullPath}", fullPath);
}
else
{
// OSS 上传
await _fileService.UploadFileStreamAsync(relativePath, pdfStream);
_logger.LogInformation("PDF文件上传到OSS: {FileName}", relativePath);
}
}
}
///
/// 文档数据模型
///
public class DocumentData
{
public List Paragraphs { get; set; } = new List();
public List Tables { get; set; } = new List();
public byte[] BackgroundImage { get; set; } = null;
public string BackgroundImageMimeType { get; set; } = string.Empty;
public byte[] QRCodeImage { get; set; } = null;
public string QRCodeMimeType { get; set; } = string.Empty;
}
///
/// 段落数据模型
///
public class ParagraphData
{
public string Text { get; set; } = string.Empty;
public string Style { get; set; } = "normal"; // normal, heading, subheading
public double? PositionX { get; set; } = null;
public double? PositionY { get; set; } = null;
public double? FontSize { get; set; } = null;
public string Alignment { get; set; } = "left"; // left, center, right
public bool IsBold { get; set; } = false;
public string FontColor { get; set; } = "black";
}
///
/// 表格数据模型
///
public class TableData
{
public List> Rows { get; set; } = new List>();
}