YD_Event.Admin/YD_Event.Application/Certificate/CertificateTemplateService.cs
2026-04-14 17:30:20 +08:00

458 lines
16 KiB
C#

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.ComponentModel.DataAnnotations;
using YD_Event.Application.Common;
namespace YD_Event.Application.Certificate;
/// <summary>
/// 证书模板管理服务
/// </summary>
public class CertificateTemplateService : BaseService<CertificateTemplate>, ITransient
{
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<CertificateTemplateService> _logger;
private readonly DocxTemplateParser _docxParser;
public CertificateTemplateService(
ISqlSugarRepository<CertificateTemplate> templateRepository,
ISqlSugarRepository<CertificatePlaceholder> placeholderRepository,
FileService fileService,
IOptionsMonitor<OssConnectionOptions> ossOptionsMonitor,
IIdGenerator idGenerator,
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor,
ILogger<CertificateTemplateService> logger,
DocxTemplateParser docxParser) : base(templateRepository)
{
_templateRepository = templateRepository;
_placeholderRepository = placeholderRepository;
_fileService = fileService;
_ossOptionsMonitor = ossOptionsMonitor;
_idGenerator = idGenerator;
_webHostEnvironment = webHostEnvironment;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_docxParser = docxParser;
}
/// <summary>
/// 上传DOC模板
/// </summary>
[DisplayName("上传DOC模板")]
public async Task<CertificateTemplateUploadResult> UploadDocTemplate([Required] IFormFile templateFile)
{
// 1. 验证文件格式
if (!IsValidDocFile(templateFile))
{
throw Oops.Oh("请上传DOC或DOCX格式的文件");
}
// 2. 上传模板文件
var templateUrl = await UploadTemplateFile(templateFile);
// 3. 检测占位符
using var templateStream = templateFile.OpenReadStream();
var placeholders = _docxParser.ExtractPlaceholders(templateStream);
// 4. 创建模板记录(自动填充字段)
var template = new CertificateTemplate
{
Id = _idGenerator.NextId(),
Version = 1,
TemplateType = "Doc",
DocxTemplateUrl = templateUrl,
Width = 210,
Height = 297,
IsActive = true,
Status = Core.Enum.AvailabilityStatus.Enable,
CreatedTime = DateTime.Now,
CreatedUserId = GetCurrentUserUserId()
};
await _templateRepository.InsertAsync(template);
// 5. 保存占位符配置
await SavePlaceholders(template.Id, placeholders);
// 6. 返回结果
var result = new CertificateTemplateUploadResult
{
TemplateId = template.Id,
TemplateUrl = templateUrl,
DetectedPlaceholders = placeholders.Select(p => new DetectedPlaceholderOutput
{
PlaceholderKey = p,
PlaceholderType = _docxParser.GetPlaceholderType(p),
IsValid = _docxParser.IsValidPlaceholder(p),
ValidationMessage = _docxParser.IsValidPlaceholder(p) ? "有效" : "格式无效",
SuggestedDisplayName = GenerateDisplayName(p)
}).ToList(),
TotalPlaceholders = placeholders.Count,
ValidPlaceholders = placeholders.Count(p => _docxParser.IsValidPlaceholder(p))
};
_logger.LogInformation("DOC模板上传成功. TemplateId: {TemplateId}, Placeholders: {Count}",
template.Id, placeholders.Count);
return result;
}
/// <summary>
/// 更新DOC模板
/// </summary>
[DisplayName("更新DOC模板")]
public async Task<CertificateTemplateUploadResult> UpdateDocTemplate(UpdateDocTemplateInput input)
{
var template = await _templateRepository.GetByIdAsync(input.Id);
if (template == null) throw Oops.Bah("模板不存在");
if (template.TemplateType != "Doc")
throw Oops.Bah("只能更新DOC类型的模板");
// 如果上传了新文件
if (input.TemplateFile != null)
{
if (!IsValidDocFile(input.TemplateFile))
{
throw Oops.Oh("请上传DOC或DOCX格式的文件");
}
// 上传新文件
var newTemplateUrl = await UploadTemplateFile(input.TemplateFile);
template.DocxTemplateUrl = newTemplateUrl;
// 重新检测占位符
using var templateStream = input.TemplateFile.OpenReadStream();
var placeholders = _docxParser.ExtractPlaceholders(templateStream);
// 更新占位符配置
await UpdatePlaceholders(template.Id, placeholders);
// 返回检测结果
var result = new CertificateTemplateUploadResult
{
TemplateId = template.Id,
TemplateUrl = newTemplateUrl,
DetectedPlaceholders = placeholders.Select(p => new DetectedPlaceholderOutput
{
PlaceholderKey = p,
PlaceholderType = _docxParser.GetPlaceholderType(p),
IsValid = _docxParser.IsValidPlaceholder(p),
ValidationMessage = _docxParser.IsValidPlaceholder(p) ? "有效" : "格式无效",
SuggestedDisplayName = GenerateDisplayName(p)
}).ToList(),
TotalPlaceholders = placeholders.Count,
ValidPlaceholders = placeholders.Count(p => _docxParser.IsValidPlaceholder(p))
};
}
// 更新其他字段
template.Version = input.Version;
template.Width = input.Width;
template.Height = input.Height;
template.IsActive = input.IsActive;
await _templateRepository.UpdateAsync(template);
_logger.LogInformation("DOC模板更新成功. TemplateId: {TemplateId}", template.Id);
// 返回更新后的模板信息
var updatedPlaceholders = await _placeholderRepository.AsQueryable()
.Where(x => x.TemplateId == template.Id)
.ToListAsync();
return new CertificateTemplateUploadResult
{
TemplateId = template.Id,
TemplateUrl = template.DocxTemplateUrl,
DetectedPlaceholders = updatedPlaceholders.Select(p => new DetectedPlaceholderOutput
{
PlaceholderKey = p.PlaceholderKey,
PlaceholderType = p.PlaceholderType,
IsValid = true,
ValidationMessage = "有效",
SuggestedDisplayName = p.DisplayName
}).ToList(),
TotalPlaceholders = updatedPlaceholders.Count,
ValidPlaceholders = updatedPlaceholders.Count
};
}
/// <summary>
/// 获取模板详情
/// </summary>
[DisplayName("获取模板详情")]
public async Task<CertificateTemplateOutput> Get(IdInput input)
{
var entity = await _templateRepository.GetByIdAsync(input.Id);
if (entity == null) throw Oops.Bah("模板不存在");
var result = entity.Adapt<CertificateTemplateOutput>();
// 获取占位符配置
var placeholders = await _placeholderRepository.AsQueryable()
.Where(x => x.TemplateId == entity.Id)
.OrderBy(x => x.Sort)
.ToListAsync();
result.Placeholders = placeholders.Adapt<List<PlaceholderOutput>>();
return result;
}
/// <summary>
/// 检测DOC模板占位符
/// </summary>
[DisplayName("检测DOC模板占位符")]
public async Task<PlaceholderDetectionResult> DetectPlaceholders(DetectPlaceholdersInput input)
{
if (!IsValidDocFile(input.TemplateFile))
{
throw Oops.Oh("请上传DOC或DOCX格式的文件");
}
var result = new PlaceholderDetectionResult();
try
{
using var templateStream = input.TemplateFile.OpenReadStream();
var placeholders = _docxParser.ExtractPlaceholders(templateStream);
foreach (var placeholder in placeholders)
{
var detected = new DetectedPlaceholderOutput
{
PlaceholderKey = placeholder,
PlaceholderType = _docxParser.GetPlaceholderType(placeholder),
IsValid = _docxParser.IsValidPlaceholder(placeholder),
ValidationMessage = _docxParser.IsValidPlaceholder(placeholder) ? "有效" : "格式无效",
SuggestedDisplayName = GenerateDisplayName(placeholder)
};
result.Placeholders.Add(detected);
if (detected.IsValid)
result.ValidCount++;
else
result.InvalidCount++;
}
_logger.LogInformation("DOC模板占位符检测完成. Total: {Total}, Valid: {Valid}, Invalid: {Invalid}",
result.TotalCount, result.ValidCount, result.InvalidCount);
}
catch (Exception ex)
{
_logger.LogError(ex, "DOC模板占位符检测失败");
throw;
}
return result;
}
/// <summary>
/// 保存占位符配置
/// </summary>
[DisplayName("保存占位符配置")]
public async Task SavePlaceholders(SavePlaceholdersInput input)
{
// 删除现有占位符
await _placeholderRepository.DeleteAsync(x => x.TemplateId == input.TemplateId);
// 添加新占位符
var placeholders = input.Placeholders.Select((p, index) => new CertificatePlaceholder
{
Id = _idGenerator.NextId(),
TemplateId = input.TemplateId,
PlaceholderKey = p.PlaceholderKey,
DisplayName = p.DisplayName,
PlaceholderType = p.PlaceholderType,
DefaultValue = p.DefaultValue,
IsRequired = p.IsRequired,
ValidationRule = p.ValidationRule,
Description = p.Description,
Sort = p.Sort > 0 ? p.Sort : index + 1
}).ToList();
await _placeholderRepository.InsertRangeAsync(placeholders);
_logger.LogInformation("占位符配置保存成功. TemplateId: {TemplateId}, Count: {Count}",
input.TemplateId, placeholders.Count);
}
/// <summary>
/// 获取模板占位符列表
/// </summary>
[DisplayName("获取模板占位符列表")]
public async Task<List<PlaceholderOutput>> GetTemplatePlaceholders(IdInput input)
{
var placeholders = await _placeholderRepository.AsQueryable()
.Where(x => x.TemplateId == input.Id)
.OrderBy(x => x.Sort)
.ToListAsync();
return placeholders.Adapt<List<PlaceholderOutput>>();
}
/// <summary>
/// 删除模板
/// </summary>
[DisplayName("删除模板")]
public async Task DeleteTemplate(DeleteCertificateInput input)
{
if (input.Ids == null || !input.Ids.Any()) return;
// 删除占位符
await _placeholderRepository.DeleteAsync(x => input.Ids.Contains(x.TemplateId));
// 删除模板
await _templateRepository.DeleteAsync(x => input.Ids.Contains(x.Id));
_logger.LogInformation("模板删除成功. Count: {Count}", input.Ids.Count);
}
/// <summary>
/// 验证是否为有效的DOC文件
/// </summary>
private bool IsValidDocFile(IFormFile file)
{
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
return extension == ".doc" || extension == ".docx";
}
/// <summary>
/// 上传模板文件
/// </summary>
private async Task<string> UploadTemplateFile(IFormFile file)
{
var options = _ossOptionsMonitor.CurrentValue;
var now = DateTime.Today;
string name = $"template_{_idGenerator.Encode(_idGenerator.NewLong())}_{file.FileName}";
string filePath = $"/templates/{now.Year}/{now.Month:D2}/{now.Day:D2}/";
string finalUrl = string.Empty;
using var stream = file.OpenReadStream();
if (!options.Enable)
{
// 预测最终 URL
string localDir = string.Concat(options.Bucket?.TrimEnd('/') ?? "", filePath);
var request = _httpContextAccessor.HttpContext?.Request;
if (request != null)
{
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir.TrimStart('/')}{name}";
}
// 本地存储
string absoluteDir = Path.Combine(_webHostEnvironment.WebRootPath, localDir.TrimStart('/'));
if (!Directory.Exists(absoluteDir))
{
Directory.CreateDirectory(absoluteDir);
}
string fullPath = Path.Combine(absoluteDir, name);
using var fileStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write);
await stream.CopyToAsync(fileStream);
}
else
{
// OSS 上传
string fileName = $"{filePath.TrimStart('/')}{name}";
finalUrl = await _fileService.UploadFileStreamAsync(fileName, stream);
}
_logger.LogInformation("模板文件上传成功. URL: {Url}", finalUrl);
return finalUrl;
}
/// <summary>
/// 保存占位符
/// </summary>
private async Task SavePlaceholders(long templateId, List<string> placeholders)
{
var placeholderEntities = placeholders.Select((p, index) => new CertificatePlaceholder
{
Id = _idGenerator.NextId(),
TemplateId = templateId,
PlaceholderKey = p,
DisplayName = GenerateDisplayName(p),
PlaceholderType = _docxParser.GetPlaceholderType(p),
IsRequired = false,
Sort = index + 1
}).ToList();
await _placeholderRepository.InsertRangeAsync(placeholderEntities);
}
/// <summary>
/// 更新占位符
/// </summary>
private async Task UpdatePlaceholders(long templateId, List<string> placeholders)
{
// 删除现有占位符
await _placeholderRepository.DeleteAsync(x => x.TemplateId == templateId);
// 保存新占位符
await SavePlaceholders(templateId, placeholders);
}
/// <summary>
/// 生成显示名称
/// </summary>
private string GenerateDisplayName(string placeholder)
{
return placeholder switch
{
"UserName" => "用户姓名",
"CourseName" => "课程名称",
"CertificateNo" => "证书编号",
"IssueDate" => "颁发日期",
"Organization" => "颁发机构",
"EventName" => "活动名称",
"EventDate" => "活动日期",
"Ranking" => "排名",
"CertificateQR" => "证书二维码",
"VerificationQR" => "验证二维码",
_ => placeholder
};
}
/// <summary>
/// 获取证书模板列表(用于选择)
/// </summary>
[DisplayName("获取证书模板列表")]
public async Task<List<CertificateTemplateSimpleOutput>> GetTemplateList()
{
var templates = await _templateRepository.AsQueryable()
.Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable && !x.DeleteMark)
.OrderByDescending(x => x.CreatedTime)
.ToListAsync();
return templates.Adapt<List<CertificateTemplateSimpleOutput>>();
}
/// <summary>
/// 获取当前用户ID
/// </summary>
private long GetCurrentUserUserId()
{
return 1;
}
}