YD_Event.Admin/YD_Event.Application/Certificate/CertificateGenerateService.cs
2026-03-20 11:59:26 +08:00

1018 lines
37 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 证书生成服务
/// </summary>
public class CertificateGenerateService : IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Certificate> _certificateRepository;
private readonly ISqlSugarRepository<CertificateTemplate> _templateRepository;
private readonly ISqlSugarRepository<CertificatePlaceholder> _placeholderRepository;
private readonly FileService _fileService;
private readonly IOptionsMonitor<OssConnectionOptions> _ossOptionsMonitor;
private readonly IIdGenerator _idGenerator;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<CertificateGenerateService> _logger;
private readonly DocxTemplateProcessor _docxProcessor;
private readonly QRCodeService _qrCodeService;
public CertificateGenerateService(
ISqlSugarRepository<YD_Event.Core.Entities.Certificate> certificateRepository,
ISqlSugarRepository<CertificateTemplate> templateRepository,
ISqlSugarRepository<CertificatePlaceholder> placeholderRepository,
FileService fileService,
IOptionsMonitor<OssConnectionOptions> ossOptionsMonitor,
IIdGenerator idGenerator,
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor,
ILogger<CertificateGenerateService> 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;
}
/// <summary>
/// 生成PDF证书
/// </summary>
[DisplayName("生成PDF证书")]
public async Task<PdfCertificateGenerationResult> 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<string, string>();
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;
}
/// <summary>
/// 批量生成PDF证书
/// </summary>
[DisplayName("批量生成PDF证书")]
public async Task<BatchPdfCertificateGenerationResult> 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;
}
/// <summary>
/// 转换DOC为PDF(使用QuestPDF和DocumentFormat.OpenXml)
/// </summary>
private async Task<Stream> 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);
}
}
/// <summary>
/// 提取DOCX文档数据(包括段落、表格、背景图片等)
/// </summary>
private async Task<DocumentData> 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<Paragraph>())
{
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<Table>())
{
var tableData = new TableData();
foreach (var row in table.Elements<TableRow>())
{
var rowData = new List<string>();
foreach (var cell in row.Elements<TableCell>())
{
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;
}
/// <summary>
/// 提取背景图片
/// </summary>
private async Task ExtractBackgroundImage(WordprocessingDocument wordDoc, DocumentData documentData)
{
try
{
var mainPart = wordDoc.MainDocumentPart;
if (mainPart != null)
{
// 查找文档中的图片
var imageParts = mainPart.GetPartsOfType<ImagePart>().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<ImagePart>();
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, "提取背景图片失败");
}
}
/// <summary>
/// 获取段落字体大小
/// </summary>
private double? GetParagraphFontSize(Paragraph paragraph)
{
var runProperties = paragraph.Elements<Run>()
.SelectMany(r => r.Elements<RunProperties>())
.FirstOrDefault();
if (runProperties?.FontSize != null)
{
var fontSize = runProperties.FontSize.Val;
if (double.TryParse(fontSize, out double size))
{
return size / 2.0; // OpenXML 中字体大小是半磅为单位
}
}
return null;
}
/// <summary>
/// 获取段落对齐方式
/// </summary>
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";
}
/// <summary>
/// 检查段落是否为粗体
/// </summary>
private bool IsParagraphBold(Paragraph paragraph)
{
var runProperties = paragraph.Elements<Run>()
.SelectMany(r => r.Elements<RunProperties>())
.FirstOrDefault();
return runProperties?.Bold != null;
}
/// <summary>
/// 获取段落字体颜色
/// </summary>
private string GetParagraphColor(Paragraph paragraph)
{
var runProperties = paragraph.Elements<Run>()
.SelectMany(r => r.Elements<RunProperties>())
.FirstOrDefault();
if (runProperties?.Color != null)
{
var color = runProperties.Color.Val;
if (!string.IsNullOrEmpty(color))
{
return color;
}
}
return "black";
}
/// <summary>
/// 获取段落位置信息
/// </summary>
private void GetParagraphPosition(Paragraph paragraph, ParagraphData paragraphData)
{
try
{
var paragraphProperties = paragraph.ParagraphProperties;
if (paragraphProperties != null)
{
_logger.LogDebug("处理段落: {Text}", paragraphData.Text);
// 检查是否有绝对定位
var frameProperties = paragraphProperties.Elements<FrameProperties>().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<Indentation>().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, "获取段落位置信息失败");
}
}
/// <summary>
/// 使用QuestPDF生成PDF
/// </summary>
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; // 为了保持异步签名
}
/// <summary>
/// 根据段落内容确定文本位置(基于A4页面210x297mm,坐标单位为点)
/// </summary>
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);
}
/// <summary>
/// 确定段落样式
/// </summary>
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";
}
/// <summary>
/// 获取LibreOffice可执行文件路径
/// </summary>
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";
}
/// <summary>
/// 保存PDF文件
/// </summary>
private async Task<string> 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;
}
/// <summary>
/// 生成二维码图片
/// </summary>
private async Task<string> 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;
}
}
/// <summary>
/// 保存二维码图片
/// </summary>
private async Task<string> 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;
}
/// <summary>
/// 生成证书URL
/// </summary>
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);
}
/// <summary>
/// 生成PDF保存地址
/// </summary>
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;
}
/// <summary>
/// 保存PDF文件到指定URL
/// </summary>
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);
}
}
}
/// <summary>
/// 文档数据模型
/// </summary>
public class DocumentData
{
public List<ParagraphData> Paragraphs { get; set; } = new List<ParagraphData>();
public List<TableData> Tables { get; set; } = new List<TableData>();
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;
}
/// <summary>
/// 段落数据模型
/// </summary>
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";
}
/// <summary>
/// 表格数据模型
/// </summary>
public class TableData
{
public List<List<string>> Rows { get; set; } = new List<List<string>>();
}