2026-03-03 16:46:02 +08:00
using YD_Event.Application.Certificate.Dtos ;
using YD_Event.Core.Entities ;
2026-03-04 12:13:49 +08:00
using YD_Event.Core.Certificate ;
2026-03-03 16:46:02 +08:00
using SqlSugar ;
using Mapster ;
using OnceMi.AspNetCore.OSS ;
using Microsoft.Extensions.Options ;
2026-03-04 12:13:49 +08:00
using YD_Event.Core.Options ;
2026-03-03 16:46:02 +08:00
using Microsoft.AspNetCore.Hosting ;
using Microsoft.AspNetCore.Http ;
using Microsoft.Extensions.Logging ;
2026-03-04 12:13:49 +08:00
using System.Diagnostics ;
using System.ComponentModel.DataAnnotations ;
using DocumentFormat.OpenXml.Packaging ;
using System.IO ;
using System.Linq ;
2026-03-03 16:46:02 +08:00
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 ;
2026-03-04 12:13:49 +08:00
private readonly ISqlSugarRepository < CertificatePlaceholder > _placeholderRepository ;
2026-03-03 16:46:02 +08:00
private readonly IOSSService _ossService ;
private readonly IOptionsMonitor < OssConnectionOptions > _ossOptionsMonitor ;
private readonly IIdGenerator _idGenerator ;
private readonly IWebHostEnvironment _webHostEnvironment ;
private readonly IHttpContextAccessor _httpContextAccessor ;
private readonly ILogger < CertificateGenerateService > _logger ;
2026-03-04 12:13:49 +08:00
private readonly DocxTemplateProcessor _docxProcessor ;
private readonly QRCodeService _qrCodeService ;
2026-03-03 16:46:02 +08:00
public CertificateGenerateService (
ISqlSugarRepository < YD_Event . Core . Entities . Certificate > certificateRepository ,
ISqlSugarRepository < CertificateTemplate > templateRepository ,
2026-03-04 12:13:49 +08:00
ISqlSugarRepository < CertificatePlaceholder > placeholderRepository ,
2026-03-03 16:46:02 +08:00
IOSSService ossService ,
IOptionsMonitor < OssConnectionOptions > ossOptionsMonitor ,
IIdGenerator idGenerator ,
IWebHostEnvironment webHostEnvironment ,
IHttpContextAccessor httpContextAccessor ,
2026-03-04 12:13:49 +08:00
ILogger < CertificateGenerateService > logger ,
DocxTemplateProcessor docxProcessor ,
QRCodeService qrCodeService )
2026-03-03 16:46:02 +08:00
{
_certificateRepository = certificateRepository ;
_templateRepository = templateRepository ;
2026-03-04 12:13:49 +08:00
_placeholderRepository = placeholderRepository ;
2026-03-03 16:46:02 +08:00
_ossService = ossService ;
_ossOptionsMonitor = ossOptionsMonitor ;
_idGenerator = idGenerator ;
_webHostEnvironment = webHostEnvironment ;
_httpContextAccessor = httpContextAccessor ;
_logger = logger ;
2026-03-04 12:13:49 +08:00
_docxProcessor = docxProcessor ;
_qrCodeService = qrCodeService ;
2026-03-03 16:46:02 +08:00
}
/// <summary>
2026-03-04 12:13:49 +08:00
/// 生成PDF证书
2026-03-03 16:46:02 +08:00
/// </summary>
2026-03-04 12:13:49 +08:00
[DisplayName("生成PDF证书")]
public async Task < PdfCertificateGenerationResult > GeneratePdf ( GeneratePdfCertificateInput input )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
var stopwatch = Stopwatch . StartNew ( ) ;
var result = new PdfCertificateGenerationResult
{
GeneratedTime = DateTime . Now ,
IsSuccess = true
} ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
try
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
// 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. 生成二维码数据
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 pdfStream = await ConvertDocToPdf ( processedStream ) ;
// 10. 保存PDF文件
var pdfUrl = await SavePdfFile ( pdfStream , certificate , template ) ;
// 11. 生成二维码图片(如果需要)
string qrCodeUrl = null ;
if ( input . GenerateQRCode )
{
qrCodeUrl = await GenerateQRCodeImage ( input . CertificateId , input . QRCodeSize ) ;
}
// 12. 设置结果
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 ) ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
catch ( Exception ex )
{
result . IsSuccess = false ;
result . ErrorMessage = ex . Message ;
_logger . LogError ( ex , "PDF证书生成失败. CertificateId: {CertificateId}" , input . CertificateId ) ;
}
finally
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
stopwatch . Stop ( ) ;
result . GenerationTimeMs = stopwatch . ElapsedMilliseconds ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
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
} ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
_logger . LogInformation ( "开始批量生成PDF证书. Count: {Count}" , input . DataList . Count ) ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
foreach ( var data in input . DataList )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
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 ) ;
}
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
result . EndTime = DateTime . Now ;
_logger . LogInformation ( "批量生成PDF证书完成. Total: {Total}, Success: {Success}, Failed: {Failed}" ,
result . TotalCount , result . SuccessCount , result . FailedCount ) ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
return result ;
}
/// <summary>
/// 转换DOC为PDF( 使用DocumentFormat.OpenXml解析并生成PDF)
/// </summary>
private async Task < Stream > ConvertDocToPdf ( Stream docStream )
{
try
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
_logger . LogInformation ( "开始DOC转PDF转换" ) ;
var pdfStream = new MemoryStream ( ) ;
string content = "" ;
try
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
// 尝试解析DOCX文件
content = await ExtractDocxContent ( docStream ) ;
_logger . LogInformation ( "DOCX解析成功, 内容长度: {Length}" , content . Length ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "DOCX解析失败, 尝试直接读取" ) ;
// 如果解析失败,直接读取流内容
docStream . Position = 0 ;
using var reader = new StreamReader ( docStream , System . Text . Encoding . UTF8 ) ;
content = await reader . ReadToEndAsync ( ) ;
}
// 如果内容为空,使用默认内容
if ( string . IsNullOrWhiteSpace ( content ) )
{
content = "Certificate Content\n\nThis is a certificate generated from DOC template.\n\nTemplate processing completed successfully." ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
// 生成PDF
await GeneratePdfFromContent ( pdfStream , content ) ;
pdfStream . Position = 0 ;
_logger . LogInformation ( "DOC转PDF完成, PDF大小: {Size} bytes" , pdfStream . Length ) ;
return pdfStream ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
catch ( Exception ex )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
_logger . LogError ( ex , "DOC转PDF失败, 使用备用方案" ) ;
return await GenerateSimplePdf ( docStream ) ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
}
/// <summary>
/// 提取DOCX文件内容
/// </summary>
private async Task < string > ExtractDocxContent ( Stream docStream )
{
var content = new System . Text . StringBuilder ( ) ;
using var wordprocessingDocument = WordprocessingDocument . Open ( docStream , false ) ;
var body = wordprocessingDocument . MainDocumentPart ? . Document ? . Body ;
if ( body ! = null )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
// 提取所有段落文本
foreach ( var paragraph in body . Elements < DocumentFormat . OpenXml . Wordprocessing . Paragraph > ( ) )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
var paragraphText = paragraph . InnerText ? . Trim ( ) ;
if ( ! string . IsNullOrEmpty ( paragraphText ) )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
content . AppendLine ( paragraphText ) ;
}
}
// 提取表格内容
foreach ( var table in body . Elements < DocumentFormat . OpenXml . Wordprocessing . Table > ( ) )
{
foreach ( var row in table . Elements < DocumentFormat . OpenXml . Wordprocessing . TableRow > ( ) )
{
var rowText = new System . Text . StringBuilder ( ) ;
foreach ( var cell in row . Elements < DocumentFormat . OpenXml . Wordprocessing . TableCell > ( ) )
{
var cellText = cell . InnerText ? . Trim ( ) ;
if ( ! string . IsNullOrEmpty ( cellText ) )
{
rowText . Append ( cellText + " | " ) ;
}
}
if ( rowText . Length > 0 )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
content . AppendLine ( rowText . ToString ( ) . TrimEnd ( " | " . ToCharArray ( ) ) ) ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
}
}
}
return content . ToString ( ) ;
}
/// <summary>
/// 从内容生成PDF
/// </summary>
private async Task GeneratePdfFromContent ( Stream pdfStream , string content )
{
using var writer = new StreamWriter ( pdfStream , System . Text . Encoding . ASCII , leaveOpen : true ) ;
// PDF头部
await writer . WriteAsync ( "%PDF-1.4\n" ) ;
// 对象1: Catalog
await writer . WriteAsync ( "1 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Type /Catalog\n" ) ;
await writer . WriteAsync ( "/Pages 2 0 R\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
// 对象2: Pages
await writer . WriteAsync ( "2 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Type /Pages\n" ) ;
await writer . WriteAsync ( "/Kids [3 0 R]\n" ) ;
await writer . WriteAsync ( "/Count 1\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
// 对象3: Page
await writer . WriteAsync ( "3 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Type /Page\n" ) ;
await writer . WriteAsync ( "/Parent 2 0 R\n" ) ;
await writer . WriteAsync ( "/MediaBox [0 0 595 842]\n" ) ;
await writer . WriteAsync ( "/Resources <<\n" ) ;
await writer . WriteAsync ( "/Font <<\n" ) ;
await writer . WriteAsync ( "/F1 <<\n" ) ;
await writer . WriteAsync ( "/Type /Font\n" ) ;
await writer . WriteAsync ( "/Subtype /Type1\n" ) ;
await writer . WriteAsync ( "/BaseFont /Arial\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "/Contents 4 0 R\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
// 准备文本内容
var lines = content . Split ( new [ ] { '\n' , '\r' } , StringSplitOptions . RemoveEmptyEntries ) ;
var textContent = new System . Text . StringBuilder ( ) ;
var yPosition = 750 ;
// 添加标题
textContent . Append ( "BT /F1 16 Tf 50 750 Td (Certificate) Tj ET\n" ) ;
yPosition - = 30 ;
// 添加内容行
foreach ( var line in lines . Take ( 20 ) ) // 限制行数避免PDF过大
{
var cleanLine = line . Replace ( "(" , "\\(" ) . Replace ( ")" , "\\)" ) . Replace ( "\r" , "" ) . Trim ( ) ;
if ( cleanLine . Length > 0 )
{
// 处理长行
if ( cleanLine . Length > 70 )
{
cleanLine = cleanLine . Substring ( 0 , 70 ) + "..." ;
}
// 处理非ASCII字符
var asciiLine = ConvertToAscii ( cleanLine ) ;
textContent . Append ( $"BT /F1 12 Tf 50 {yPosition} Td ({asciiLine}) Tj ET\n" ) ;
yPosition - = 18 ; // 行间距
// 如果位置太低,停止添加
if ( yPosition < 50 ) break ;
}
}
var contentText = textContent . ToString ( ) ;
var contentBytes = System . Text . Encoding . ASCII . GetBytes ( contentText ) ;
// 对象4: Content Stream
await writer . WriteAsync ( "4 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( $"/Length {contentBytes.Length}\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "stream\n" ) ;
await writer . FlushAsync ( ) ;
// 直接写入字节到流
pdfStream . Write ( contentBytes , 0 , contentBytes . Length ) ;
await writer . WriteAsync ( "\nendstream\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
// 记录当前位置用于xref
var currentPos = pdfStream . Length ;
// 交叉引用表
var xrefOffset = pdfStream . Length ;
await writer . WriteAsync ( "xref\n" ) ;
await writer . WriteAsync ( "0 5\n" ) ;
await writer . WriteAsync ( "0000000000 65535 f\n" ) ;
await writer . WriteAsync ( "0000000010 00000 n\n" ) ;
await writer . WriteAsync ( "0000000079 00000 n\n" ) ;
await writer . WriteAsync ( "0000000173 00000 n\n" ) ;
await writer . WriteAsync ( $"000000{currentPos.ToString().PadLeft(6, '0')} 00000 n\n" ) ;
// Trailer
await writer . WriteAsync ( "trailer\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Size 5\n" ) ;
await writer . WriteAsync ( "/Root 1 0 R\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "startxref\n" ) ;
await writer . WriteAsync ( $"{xrefOffset}\n" ) ;
await writer . WriteAsync ( "%%EOF\n" ) ;
await writer . FlushAsync ( ) ;
}
/// <summary>
/// 转换文本为ASCII兼容格式
/// </summary>
private string ConvertToAscii ( string text )
{
var result = new System . Text . StringBuilder ( ) ;
foreach ( char c in text )
{
if ( c < 128 ) // ASCII字符
{
result . Append ( c ) ;
}
else
{
// 非ASCII字符转换为占位符
result . Append ( "?" ) ;
}
}
return result . ToString ( ) ;
}
/// <summary>
/// 生成简单的PDF作为备用方案
/// </summary>
private async Task < Stream > GenerateSimplePdf ( Stream docStream )
{
_logger . LogInformation ( "使用备用PDF生成方案" ) ;
var pdfStream = new MemoryStream ( ) ;
try
{
// 重置流位置
docStream . Position = 0 ;
// 读取DOC内容
string content ;
try
{
using var reader = new StreamReader ( docStream , System . Text . Encoding . UTF8 ) ;
content = await reader . ReadToEndAsync ( ) ;
}
catch
{
// 如果无法读取DOC内容, 使用默认内容
content = "Original document content could not be read." ;
}
// 创建简单的PDF内容
using var writer = new StreamWriter ( pdfStream , System . Text . Encoding . UTF8 , leaveOpen : true ) ;
await writer . WriteAsync ( "%PDF-1.4\n" ) ;
await writer . WriteAsync ( "1 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Type /Catalog\n" ) ;
await writer . WriteAsync ( "/Pages 2 0 R\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
await writer . WriteAsync ( "2 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Type /Pages\n" ) ;
await writer . WriteAsync ( "/Kids [3 0 R]\n" ) ;
await writer . WriteAsync ( "/Count 1\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
await writer . WriteAsync ( "3 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Type /Page\n" ) ;
await writer . WriteAsync ( "/Parent 2 0 R\n" ) ;
await writer . WriteAsync ( "/MediaBox [0 0 595 842]\n" ) ;
await writer . WriteAsync ( "/Resources <<\n" ) ;
await writer . WriteAsync ( "/Font <<\n" ) ;
await writer . WriteAsync ( "/F1 <<\n" ) ;
await writer . WriteAsync ( "/Type /Font\n" ) ;
await writer . WriteAsync ( "/Subtype /Type1\n" ) ;
await writer . WriteAsync ( "/BaseFont /Arial\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( $"/Contents 4 0 R\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
// 简化的内容(只显示基本信息)
var displayContent = content . Length > 0
? content . Substring ( 0 , Math . Min ( 500 , content . Length ) )
: "Document content is empty or could not be read." ;
var simpleContent = "Certificate Generated\n\nNote: This is a simplified PDF generated\nbecause LibreOffice is not available.\n\nOriginal document content:\n" + displayContent ;
var contentBytes = System . Text . Encoding . UTF8 . GetBytes ( simpleContent ) ;
await writer . WriteAsync ( "4 0 obj\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( $"/Length {contentBytes.Length}\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "stream\n" ) ;
await writer . FlushAsync ( ) ;
pdfStream . Write ( contentBytes , 0 , contentBytes . Length ) ;
await writer . WriteAsync ( "\nendstream\n" ) ;
await writer . WriteAsync ( "endobj\n" ) ;
await writer . WriteAsync ( "xref\n" ) ;
await writer . WriteAsync ( "0 5\n" ) ;
await writer . WriteAsync ( "0000000000 65535 f\n" ) ;
await writer . WriteAsync ( "0000000010 00000 n\n" ) ;
await writer . WriteAsync ( "0000000079 00000 n\n" ) ;
await writer . WriteAsync ( "0000000173 00000 n\n" ) ;
// 计算正确的偏移量
var xrefOffset = pdfStream . Length ;
await writer . WriteAsync ( $"000000{Math.Max(300, contentBytes.Length + 200).ToString().PadLeft(6, '0')} 00000 n\n" ) ;
await writer . WriteAsync ( "trailer\n" ) ;
await writer . WriteAsync ( "<<\n" ) ;
await writer . WriteAsync ( "/Size 5\n" ) ;
await writer . WriteAsync ( "/Root 1 0 R\n" ) ;
await writer . WriteAsync ( ">>\n" ) ;
await writer . WriteAsync ( "startxref\n" ) ;
await writer . WriteAsync ( $"{xrefOffset}\n" ) ;
await writer . WriteAsync ( "%%EOF\n" ) ;
await writer . FlushAsync ( ) ;
_logger . LogInformation ( "备用PDF生成完成, 大小: {Size} bytes" , pdfStream . Length ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "备用PDF生成也失败了" ) ;
throw new Exception ( "PDF生成完全失败" , ex ) ;
}
pdfStream . Position = 0 ;
return pdfStream ;
}
/// <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 ( 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 ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
pdfStream . Position = 0 ;
2026-03-03 16:46:02 +08:00
if ( ! options . Enable )
{
// 本地存储
string localDir = string . Concat ( options . Bucket . TrimEnd ( '/' ) , filePath ) ;
string absoluteDir = Path . Combine ( _webHostEnvironment . WebRootPath , localDir ) ;
if ( ! Directory . Exists ( absoluteDir ) )
{
Directory . CreateDirectory ( absoluteDir ) ;
}
2026-03-04 12:13:49 +08:00
string fullPath = Path . Combine ( absoluteDir , name ) ;
// 预测最终 URL
var request = _httpContextAccessor . HttpContext ? . Request ;
if ( request ! = null )
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}" ;
}
using var fileStream = new FileStream ( fullPath , FileMode . Create , FileAccess . Write ) ;
await pdfStream . CopyToAsync ( fileStream ) ;
_logger . LogInformation ( "PDF文件保存到本地: {FullPath}" , fullPath ) ;
2026-03-03 16:46:02 +08:00
}
else
{
// OSS 上传
2026-03-04 12:13:49 +08:00
string fileName = $"{filePath}{name}" ;
await _ossService . PutObjectAsync ( options . Bucket , fileName , pdfStream ) ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
// 预测最终 URL
string bucketPrefix = ( options . Provider = = OSSProvider . Minio ) ? $"/{options.Bucket}" : string . Empty ;
finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}" ;
_logger . LogInformation ( "PDF文件上传到OSS: {FileName}" , fileName ) ;
2026-03-03 16:46:02 +08:00
}
return finalUrl ;
}
2026-03-04 12:13:49 +08:00
/// <summary>
/// 生成二维码图片
/// </summary>
private async Task < string > GenerateQRCodeImage ( long certificateId , int size )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
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 ;
}
}
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
/// <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 ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
// 预测最终 URL
if ( ! options . Enable )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
string localDir = string . Concat ( options . Bucket . TrimEnd ( '/' ) , filePath ) ;
var request = _httpContextAccessor . HttpContext ? . Request ;
if ( request ! = null )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}{extension}" ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
}
else
{
string bucketPrefix = ( options . Provider = = OSSProvider . Minio ) ? $"/{options.Bucket}" : string . Empty ;
string fileName = $"{filePath}{name}{extension}" ;
finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}" ;
}
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
qrStream . Position = 0 ;
2026-03-03 16:46:02 +08:00
2026-03-04 12:13:49 +08:00
if ( ! options . Enable )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
// 本地存储
string localDir = string . Concat ( options . Bucket . TrimEnd ( '/' ) , filePath ) ;
string absoluteDir = Path . Combine ( _webHostEnvironment . WebRootPath , localDir ) ;
if ( ! Directory . Exists ( absoluteDir ) )
2026-03-03 16:46:02 +08:00
{
2026-03-04 12:13:49 +08:00
Directory . CreateDirectory ( absoluteDir ) ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
string fullPath = Path . Combine ( absoluteDir , $"{name}{extension}" ) ;
using var fileStream2 = new FileStream ( fullPath , FileMode . Create , FileAccess . Write ) ;
await qrStream . CopyToAsync ( fileStream2 ) ;
}
else
{
// OSS 上传
string fileName = $"{filePath}{name}{extension}" ;
await _ossService . PutObjectAsync ( options . Bucket , fileName , qrStream ) ;
2026-03-03 16:46:02 +08:00
}
2026-03-04 12:13:49 +08:00
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 ) ;
2026-03-03 16:46:02 +08:00
}
}