459 lines
16 KiB
C#
459 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>, IDynamicApiController, 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.SoftDeleteAsync(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.SoftDeleteAsync(x => input.Ids.Contains(x.TemplateId));
|
|
|
|
// 软删模板
|
|
await _templateRepository.SoftDeleteByIdsAsync(input.Ids);
|
|
|
|
_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.SoftDeleteAsync(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>
|
|
[HttpGet("gettemplatelist")]
|
|
[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;
|
|
}
|
|
}
|