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 DocumentFormat.OpenXml.Drawing.Wordprocessing;
using System.IO;
using System.Linq;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using YD_Event.Application.Common;
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 placeholderMapping = new Dictionary();
foreach (var placeholder in placeholders)
{
if (!string.IsNullOrWhiteSpace(placeholder.DisplayName))
{
placeholderMapping[placeholder.DisplayName] = placeholder.PlaceholderKey;
}
}
_docxProcessor.SetPlaceholderMapping(placeholderMapping);
_logger.LogInformation("设置占位符映射完成. 映射数量: {Count}", placeholderMapping.Count);
// 9. 处理模板
var processedStream = _docxProcessor.ProcessTemplate(templateStream, input.Data, qrCodeData);
// 10. 先生成证书保存地址(预设的PDF地址)
var pdfUrl = GeneratePdfUrl(input.CertificateId, certificate, template);
// 11. 生成二维码图片(使用预设的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();
}
// 12. 转换为PDF(包含二维码和数据替换)
var pdfStream = await ConvertDocToPdf(processedStream, qrCodeImage, input.Data, qrCodeData, placeholderMapping);
// 13. 保存PDF文件到预设地址
await SavePdfFileToUrl(pdfStream, pdfUrl);
// 14. 生成二维码图片(如果需要)
string qrCodeUrl = null;
if (input.GenerateQRCode)
{
qrCodeUrl = await GenerateQRCodeImage(input.CertificateId, input.QRCodeSize);
}
// 15. 设置结果
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)
///
private async Task ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = null, Dictionary data = null, Dictionary qrCodeData = null, Dictionary placeholderMapping = null)
{
_logger.LogInformation("开始DOCX转PDF转换(使用QuestPDF)");
// 重置流位置
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";
}
// 替换占位符内容
if (data != null)
{
ReplacePlaceholdersInDocumentData(documentData, data, qrCodeData, placeholderMapping);
}
// 生成PDF
var pdfStream = new MemoryStream();
await GeneratePdfWithQuestPDF(pdfStream, documentData);
pdfStream.Position = 0;
_logger.LogInformation("DOCX转PDF完成,PDF大小: {Size} bytes", pdfStream.Length);
return pdfStream;
}
///
/// 替换DocumentData中的占位符
///
private void ReplacePlaceholdersInDocumentData(DocumentData documentData, Dictionary data, Dictionary qrCodeData, Dictionary placeholderMapping)
{
const string placeholderPattern = @"\{\{([^}]+)\}\}";
// 记录占位符映射信息(便于调试)
if (placeholderMapping != null && placeholderMapping.Count > 0)
{
_logger.LogDebug("占位符映射配置: {Mapping}", string.Join(", ", placeholderMapping.Select(kvp => $"{kvp.Key}->{kvp.Value}")));
}
// 记录数据字典中的key(便于调试)
if (data != null && data.Count > 0)
{
_logger.LogDebug("数据字典中的key: {Keys}", string.Join(", ", data.Keys));
}
foreach (var paragraph in documentData.Paragraphs)
{
var originalText = paragraph.Text;
var processedText = originalText;
// 使用正则表达式替换所有占位符
processedText = System.Text.RegularExpressions.Regex.Replace(processedText, placeholderPattern, match =>
{
var key = match.Groups[1].Value.Trim();
_logger.LogDebug("处理占位符: {Key}", key);
// 优先使用二维码数据
if (qrCodeData != null && qrCodeData.ContainsKey(key))
{
_logger.LogDebug(" -> 使用二维码数据: {Value}", qrCodeData[key]);
return qrCodeData[key];
}
// 尝试使用占位符映射(中文显示名 -> 英文键名)
string mappedKey = key;
if (placeholderMapping != null && placeholderMapping.ContainsKey(key))
{
mappedKey = placeholderMapping[key];
_logger.LogDebug(" -> 占位符映射: {Original} -> {Mapped}", key, mappedKey);
}
// 使用普通数据(先尝试映射后的key,再尝试原始key)
if (data != null)
{
if (data.ContainsKey(mappedKey))
{
_logger.LogDebug(" -> 使用数据字典(映射后): {Key} = {Value}", mappedKey, data[mappedKey]);
return data[mappedKey] ?? string.Empty;
}
if (data.ContainsKey(key))
{
_logger.LogDebug(" -> 使用数据字典(原始): {Key} = {Value}", key, data[key]);
return data[key] ?? string.Empty;
}
}
// 返回原始占位符(未找到数据)
_logger.LogDebug(" -> 未找到数据,保留原始占位符");
return match.Value;
});
if (originalText != processedText)
{
_logger.LogDebug("占位符替换: {Original} -> {Processed}", originalText, processedText);
paragraph.Text = processedText;
}
}
}
///
/// 提取DOCX文档数据(包括段落、表格、背景图片等)
///
private async Task ExtractDocumentData(Stream docStream)
{
var documentData = new DocumentData();
var processedTexts = new HashSet(); // 记录已处理的文本,避免重复
using var wordprocessingDocument = WordprocessingDocument.Open(docStream, false);
var body = wordprocessingDocument.MainDocumentPart?.Document?.Body;
// 提取背景图片
await ExtractBackgroundImage(wordprocessingDocument, documentData);
if (body != null)
{
// 首先处理 Drawing 对象(浮于文字上方的文本框)
ExtractDrawingObjects(body, documentData, processedTexts);
// 提取段落(包含位置信息),但跳过包含Drawing对象的段落(避免重复)
foreach (var paragraph in body.Elements())
{
// 检查段落是否包含 Drawing 对象,如果是则跳过(已在 ExtractDrawingObjects 中处理)
var hasDrawing = paragraph.Descendants().Any();
if (hasDrawing)
{
_logger.LogDebug("跳过包含 Drawing 对象的段落");
continue;
}
var paragraphText = paragraph.InnerText?.Trim();
if (!string.IsNullOrEmpty(paragraphText))
{
// 增强过滤逻辑,过滤掉不应该出现的内容
if (ShouldSkipParagraph(paragraphText))
{
_logger.LogDebug("跳过不应该的内容: {Text}", paragraphText);
continue;
}
// 清理段落文本,移除不需要的内容
var cleanedText = CleanParagraphText(paragraphText);
if (string.IsNullOrEmpty(cleanedText))
{
continue;
}
// 检查是否已经处理过相同文本,避免重复
if (processedTexts.Contains(cleanedText))
{
_logger.LogDebug("跳过重复文本: {Text}", cleanedText);
continue;
}
var paragraphData = new ParagraphData
{
Text = cleanedText,
Style = DetermineParagraphStyle(paragraph),
FontSize = GetParagraphFontSize(paragraph),
Alignment = GetParagraphAlignment(paragraph),
IsBold = IsParagraphBold(paragraph),
FontColor = GetParagraphColor(paragraph)
};
// 尝试获取位置信息
GetParagraphPosition(paragraph, paragraphData);
_logger.LogDebug("提取段落: Text={Text}, X={X}, Y={Y}",
cleanedText, paragraphData.PositionX, paragraphData.PositionY);
documentData.Paragraphs.Add(paragraphData);
processedTexts.Add(cleanedText);
}
}
// 提取表格
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 bool ShouldSkipParagraph(string text)
{
if (string.IsNullOrEmpty(text)) return true;
// 过滤掉包含明显内部元数据的内容
var lowerText = text.ToLower();
// 过滤引用相关内容
if (lowerText.Contains("citation")) return true;
// 过滤纯数字的定位信息
if (System.Text.RegularExpressions.Regex.IsMatch(text, @"^\d+$")) return true;
// 过滤定位前缀(left-, right-, top-, bottom-)
if (text.StartsWith("right-") || text.StartsWith("left-") ||
text.StartsWith("top-") || text.StartsWith("bottom-")) return true;
return false;
}
///
/// 清理段落文本,移除不需要的内容
///
private string CleanParagraphText(string text)
{
if (string.IsNullOrEmpty(text)) return text;
var cleaned = text;
// 移除连续的数字序列(定位信息)
cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"\d{6,}", "");
// 移除 left-xxx, right-xxx, top-xxx, bottom-xxx 这类定位信息
cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"(left-|right-|top-|bottom-)\d+", "");
// 移除重复的占位符(如 {{单位}}00{{单位}})
var placeholderPattern = @"\{\{([^}]+)\}\}";
var matches = System.Text.RegularExpressions.Regex.Matches(cleaned, placeholderPattern);
if (matches.Count > 1)
{
// 保留第一个占位符,移除后续重复的
var firstMatch = matches[0];
cleaned = cleaned.Substring(0, firstMatch.Index + firstMatch.Length);
}
// 移除残留的 "00" 这类分隔符
cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"00+", "");
// 移除首尾空格
cleaned = cleaned.Trim();
return cleaned;
}
///
/// 提取 Drawing 对象(浮于文字上方的文本框)
///
private void ExtractDrawingObjects(Body body, DocumentData documentData, HashSet processedTexts = null)
{
try
{
// 查找所有包含 Drawing 的段落
foreach (var paragraph in body.Elements())
{
// 查找段落中的 Drawing 元素
var drawings = paragraph.Descendants().ToList();
foreach (var drawing in drawings)
{
ExtractTextFromDrawing(drawing, documentData, processedTexts);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "提取 Drawing 对象失败");
}
}
///
/// 从 Drawing 对象中提取文本和位置信息
///
private void ExtractTextFromDrawing(DocumentFormat.OpenXml.Wordprocessing.Drawing drawing, DocumentData documentData, HashSet processedTexts = null)
{
try
{
// 尝试从 Drawing 对象中提取文本框内容
var textBoxContents = drawing.Descendants().ToList();
foreach (var textBoxContent in textBoxContents)
{
// 提取文本框中的所有段落
var paragraphs = textBoxContent.Elements().ToList();
foreach (var paragraph in paragraphs)
{
var paragraphText = paragraph.InnerText?.Trim();
if (!string.IsNullOrEmpty(paragraphText))
{
// 增强过滤逻辑,过滤掉不应该出现的内容
if (ShouldSkipParagraph(paragraphText))
{
_logger.LogDebug("跳过 Drawing 对象中的内容: {Text}", paragraphText);
continue;
}
// 清理段落文本,移除不需要的内容
var cleanedText = CleanParagraphText(paragraphText);
if (string.IsNullOrEmpty(cleanedText))
{
continue;
}
// 检查是否已经处理过相同文本,避免重复
if (processedTexts != null && processedTexts.Contains(cleanedText))
{
_logger.LogDebug("跳过 Drawing 对象中的重复文本: {Text}", cleanedText);
continue;
}
var paragraphData = new ParagraphData
{
Text = cleanedText,
Style = DetermineParagraphStyle(paragraph),
FontSize = GetParagraphFontSize(paragraph),
Alignment = GetParagraphAlignment(paragraph),
IsBold = IsParagraphBold(paragraph),
FontColor = GetParagraphColor(paragraph)
};
// 尝试从 Drawing 对象中提取位置信息
ExtractPositionFromDrawing(drawing, paragraphData);
_logger.LogDebug("从 Drawing 对象提取段落: Text={Text}, X={X}, Y={Y}",
cleanedText, paragraphData.PositionX, paragraphData.PositionY);
documentData.Paragraphs.Add(paragraphData);
if (processedTexts != null)
{
processedTexts.Add(cleanedText);
}
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "从 Drawing 对象提取文本失败");
}
}
///
/// 从 Drawing 对象中提取位置信息
///
private void ExtractPositionFromDrawing(DocumentFormat.OpenXml.Wordprocessing.Drawing drawing, ParagraphData paragraphData)
{
try
{
_logger.LogDebug("开始解析 Drawing 对象位置信息");
var outerXml = drawing.OuterXml;
_logger.LogDebug("Drawing 对象原始 XML (前800字符): {Xml}", outerXml.Substring(0, Math.Min(800, outerXml.Length)));
// 方法1: 查找 simplePos 属性(通常用于绝对定位)
ExtractSimplePosition(outerXml, paragraphData);
// 方法2: 如果 simplePos 没有值,查找 positionOffset 元素
if (paragraphData.PositionX == null || paragraphData.PositionY == null)
{
ExtractPositionOffset(outerXml, paragraphData);
}
// 注意:不使用 distL, distT 等距离属性
// 这些属性不是文本框在页面上的坐标,而是:
// - 要么是文本框的内边距(文本到边框的距离)
// - 要么是文本框到页面边距的距离(而不是绝对坐标)
// 我们目前使用写死定位方式,不需要依赖这些值
_logger.LogDebug("最终提取的位置 - X: {X}, Y: {Y}", paragraphData.PositionX, paragraphData.PositionY);
}
catch (Exception ex)
{
_logger.LogError(ex, "从 Drawing 对象提取位置信息失败");
}
}
///
/// 从 XML 中提取 simplePos 属性
///
private void ExtractSimplePosition(string xml, ParagraphData paragraphData)
{
try
{
// 查找 simplePos:x 和 simplePos:y 属性
var xMatch = System.Text.RegularExpressions.Regex.Match(xml, @"simplePos:x=[""'](\d+)[""']");
var yMatch = System.Text.RegularExpressions.Regex.Match(xml, @"simplePos:y=[""'](\d+)[""']");
if (xMatch.Success)
{
if (long.TryParse(xMatch.Groups[1].Value, out long x))
{
paragraphData.PositionX = EmuToPoints(x);
_logger.LogDebug("simplePos X (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX);
}
}
if (yMatch.Success)
{
if (long.TryParse(yMatch.Groups[1].Value, out long y))
{
paragraphData.PositionY = EmuToPoints(y);
_logger.LogDebug("simplePos Y (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "提取 simplePos 失败");
}
}
///
/// 从 XML 中提取 positionOffset 元素
///
private void ExtractPositionOffset(string xml, ParagraphData paragraphData)
{
try
{
// 分别提取 positionH 和 positionV 中的 posOffset 和 relativeFrom
// 这样可以更准确地获取 X 和 Y 坐标及其参照对象
var positionHPattern = @"]*relativeFrom=[""']([^""']+)[""'][^>]*>(?:\s*]*>(\d+)\s*)";
var positionHMatch = System.Text.RegularExpressions.Regex.Match(
xml,
positionHPattern,
System.Text.RegularExpressions.RegexOptions.Singleline);
var positionVPattern = @"]*relativeFrom=[""']([^""']+)[""'][^>]*>(?:\s*]*>(\d+)\s*)";
var positionVMatch = System.Text.RegularExpressions.Regex.Match(
xml,
positionVPattern,
System.Text.RegularExpressions.RegexOptions.Singleline);
// 日志输出参照对象信息
if (positionHMatch.Success)
{
_logger.LogDebug("positionH 参照对象: {RelativeFrom}", positionHMatch.Groups[1].Value);
}
if (positionVMatch.Success)
{
_logger.LogDebug("positionV 参照对象: {RelativeFrom}", positionVMatch.Groups[1].Value);
}
if (positionHMatch.Success && paragraphData.PositionX == null)
{
if (long.TryParse(positionHMatch.Groups[2].Value, out long x))
{
paragraphData.PositionX = EmuToPoints(x);
_logger.LogDebug("positionH posOffset (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX);
}
}
if (positionVMatch.Success && paragraphData.PositionY == null)
{
if (long.TryParse(positionVMatch.Groups[2].Value, out long y))
{
paragraphData.PositionY = EmuToPoints(y);
_logger.LogDebug("positionV posOffset (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY);
}
}
// 如果上面的方法失败,尝试简单的正则表达式匹配
if (!positionHMatch.Success || !positionVMatch.Success)
{
_logger.LogDebug("尝试备用方案:简单匹配 posOffset");
var allPosOffsetMatches = System.Text.RegularExpressions.Regex.Matches(xml, @"]*>(\d+)");
if (allPosOffsetMatches.Count >= 2)
{
if (long.TryParse(allPosOffsetMatches[0].Groups[1].Value, out long x) && paragraphData.PositionX == null)
{
paragraphData.PositionX = EmuToPoints(x);
_logger.LogDebug("备用方案 - posOffset X (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX);
}
if (long.TryParse(allPosOffsetMatches[1].Groups[1].Value, out long y) && paragraphData.PositionY == null)
{
paragraphData.PositionY = EmuToPoints(y);
_logger.LogDebug("备用方案 - posOffset Y (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "提取 positionOffset 失败");
}
}
///
/// 从 XML 中提取 distL, distT 等距离属性
///
private void ExtractDistanceAttributes(string xml, ParagraphData paragraphData)
{
try
{
// 查找 distL (left distance) 和 distT (top distance)
var distLMatch = System.Text.RegularExpressions.Regex.Match(xml, @"distL=[""'](\d+)[""']");
var distTMatch = System.Text.RegularExpressions.Regex.Match(xml, @"distT=[""'](\d+)[""']");
if (distLMatch.Success && paragraphData.PositionX == null)
{
if (long.TryParse(distLMatch.Groups[1].Value, out long x))
{
paragraphData.PositionX = EmuToPoints(x);
_logger.LogDebug("distL X (EMU): {EMU}, (点): {Points}", x, paragraphData.PositionX);
}
}
if (distTMatch.Success && paragraphData.PositionY == null)
{
if (long.TryParse(distTMatch.Groups[1].Value, out long y))
{
paragraphData.PositionY = EmuToPoints(y);
_logger.LogDebug("distT Y (EMU): {EMU}, (点): {Points}", y, paragraphData.PositionY);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "提取距离属性失败");
}
}
///
/// EMU (English Metric Unit) 转换为点
/// 1 英寸 = 914400 EMU
/// 1 英寸 = 72 点
/// 所以:1 EMU = 72 / 914400 点 = 1 / 12700 点
///
private double EmuToPoints(long emu)
{
return emu / 12700.0;
}
///
/// 获取段落位置信息
///
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();
}
// 使用 Column 来放置所有内容,但使用 Translate 而非 Padding 定位
page.Content().Column(column =>
{
// 根据段落信息进行定位(使用从Word文档提取的位置信息)
foreach (var paragraph in documentData.Paragraphs)
{
var fontSize = paragraph.FontSize ?? 14;
var isBold = paragraph.IsBold;
// 日志输出段落信息,便于调试
_logger.LogDebug("处理段落: Text={Text}, X={X}, Y={Y}, FontSize={FontSize}, Bold={IsBold}",
paragraph.Text, paragraph.PositionX, paragraph.PositionY, fontSize, isBold);
// 直接使用从Word文档提取的位置信息,并增加110个单位进行调整
float x = (float)(paragraph.PositionX ?? 50) + 90;
float y = (float)(paragraph.PositionY ?? 50) + 50;
_logger.LogDebug("最终位置 - X={X}, Y={Y}", x, y);
// 使用 Translate 而非 Padding 来定位,避免流式布局累加导致分页
var textElement = column.Item().TranslateX(x).TranslateY(y).Text(paragraph.Text)
.FontSize((float)fontSize)
.FontColor(QuestPDF.Helpers.Colors.Black)
.AlignLeft();
if (isBold)
{
textElement.Bold();
}
}
});
// 添加二维码到右下角(使用 Footer)
if (documentData.QRCodeImage != null)
{
float qrSize = 100;
_logger.LogDebug("添加二维码: Size={Size}", qrSize);
page.Footer().AlignRight().PaddingRight(30).PaddingBottom(30)
.Width(qrSize).Height(qrSize)
.Image(documentData.QRCodeImage);
}
});
});
// 生成PDF到流
questDocument.GeneratePdf(pdfStream);
await Task.CompletedTask; // 为了保持异步签名
}
///
/// 确定段落样式
///
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";
}
///
/// 保存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>();
}