456 lines
16 KiB
C#
456 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 OnceMi.AspNetCore.OSS;
|
||
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;
|
||
|
||
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 IOSSService _ossService;
|
||
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,
|
||
IOSSService ossService,
|
||
IOptionsMonitor<OssConnectionOptions> ossOptionsMonitor,
|
||
IIdGenerator idGenerator,
|
||
IWebHostEnvironment webHostEnvironment,
|
||
IHttpContextAccessor httpContextAccessor,
|
||
ILogger<CertificateTemplateService> logger,
|
||
DocxTemplateParser docxParser) : base(templateRepository)
|
||
{
|
||
_templateRepository = templateRepository;
|
||
_placeholderRepository = placeholderRepository;
|
||
_ossService = ossService;
|
||
_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, // 自动设置为1
|
||
TemplateType = "Doc",
|
||
DocxTemplateUrl = templateUrl,
|
||
Width = 210, // A4纸宽度(毫米)
|
||
Height = 297, // A4纸高度(毫米)
|
||
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;
|
||
|
||
// 预测最终 URL
|
||
if (!options.Enable)
|
||
{
|
||
string localDir = string.Concat(options.Bucket.TrimEnd('/'), filePath);
|
||
var request = _httpContextAccessor.HttpContext?.Request;
|
||
if (request != null)
|
||
{
|
||
finalUrl = $"{request.Scheme}://{request.Host.Value}/{localDir}{name}";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
string bucketPrefix = (options.Provider == OSSProvider.Minio) ? $"/{options.Bucket}" : string.Empty;
|
||
string fileName = $"{filePath}{name}";
|
||
finalUrl = $"{options.Domain.TrimEnd('/')}{bucketPrefix}{fileName}";
|
||
}
|
||
|
||
using var stream = file.OpenReadStream();
|
||
|
||
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);
|
||
}
|
||
|
||
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}{name}";
|
||
await _ossService.PutObjectAsync(options.Bucket, 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>
|
||
/// 获取当前用户ID
|
||
/// </summary>
|
||
private long GetCurrentUserUserId()
|
||
{
|
||
// 这里需要根据实际的认证机制获取当前用户ID
|
||
// 暂时返回1,实际项目中需要实现
|
||
return 1;
|
||
}
|
||
}
|