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;
///
/// 证书模板管理服务
///
public class CertificateTemplateService : BaseService, IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository _templateRepository;
private readonly ISqlSugarRepository _placeholderRepository;
private readonly FileService _fileService;
private readonly IOptionsMonitor _ossOptionsMonitor;
private readonly IIdGenerator _idGenerator;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger _logger;
private readonly DocxTemplateParser _docxParser;
public CertificateTemplateService(
ISqlSugarRepository templateRepository,
ISqlSugarRepository placeholderRepository,
FileService fileService,
IOptionsMonitor ossOptionsMonitor,
IIdGenerator idGenerator,
IWebHostEnvironment webHostEnvironment,
IHttpContextAccessor httpContextAccessor,
ILogger logger,
DocxTemplateParser docxParser) : base(templateRepository)
{
_templateRepository = templateRepository;
_placeholderRepository = placeholderRepository;
_fileService = fileService;
_ossOptionsMonitor = ossOptionsMonitor;
_idGenerator = idGenerator;
_webHostEnvironment = webHostEnvironment;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_docxParser = docxParser;
}
///
/// 上传DOC模板
///
[DisplayName("上传DOC模板")]
public async Task 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
};
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;
}
///
/// 更新DOC模板
///
[DisplayName("更新DOC模板")]
public async Task 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
};
}
///
/// 获取模板详情
///
[DisplayName("获取模板详情")]
public async Task Get(IdInput input)
{
var entity = await _templateRepository.GetByIdAsync(input.Id);
if (entity == null) throw Oops.Bah("模板不存在");
var result = entity.Adapt();
// 获取占位符配置
var placeholders = await _placeholderRepository.AsQueryable()
.Where(x => x.TemplateId == entity.Id)
.OrderBy(x => x.Sort)
.ToListAsync();
result.Placeholders = placeholders.Adapt>();
return result;
}
///
/// 检测DOC模板占位符
///
[DisplayName("检测DOC模板占位符")]
public async Task 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;
}
///
/// 保存占位符配置
///
[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);
}
///
/// 获取模板占位符列表
///
[DisplayName("获取模板占位符列表")]
public async Task> GetTemplatePlaceholders(IdInput input)
{
var placeholders = await _placeholderRepository.AsQueryable()
.Where(x => x.TemplateId == input.Id)
.OrderBy(x => x.Sort)
.ToListAsync();
return placeholders.Adapt>();
}
///
/// 删除模板
///
[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);
}
///
/// 验证是否为有效的DOC文件
///
private bool IsValidDocFile(IFormFile file)
{
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
return extension == ".doc" || extension == ".docx";
}
///
/// 上传模板文件
///
private async Task 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;
}
///
/// 保存占位符
///
private async Task SavePlaceholders(long templateId, List 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);
}
///
/// 更新占位符
///
private async Task UpdatePlaceholders(long templateId, List placeholders)
{
// 软删现有占位符
await _placeholderRepository.SoftDeleteAsync(x => x.TemplateId == templateId);
// 保存新占位符
await SavePlaceholders(templateId, placeholders);
}
///
/// 生成显示名称
///
private string GenerateDisplayName(string placeholder)
{
return placeholder switch
{
"UserName" => "用户姓名",
"CourseName" => "课程名称",
"CertificateNo" => "证书编号",
"IssueDate" => "颁发日期",
"Organization" => "颁发机构",
"EventName" => "活动名称",
"EventDate" => "活动日期",
"Ranking" => "排名",
"CertificateQR" => "证书二维码",
"VerificationQR" => "验证二维码",
_ => placeholder
};
}
///
/// 获取证书模板列表(用于选择)
///
[HttpGet("gettemplatelist")]
[DisplayName("获取证书模板列表")]
public async Task> GetTemplateList()
{
var templates = await _templateRepository.AsQueryable()
.Where(x => x.Status == Core.Enum.AvailabilityStatus.Enable && !x.DeleteMark)
.OrderByDescending(x => x.CreatedTime)
.ToListAsync();
return templates.Adapt>();
}
}