YD_Event.Admin/YD_Event.Application/Certificate/CertificateGenerateService.cs

765 lines
28 KiB
C#
Raw Normal View History

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