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.File; namespace YD_Event.Application.Certificate; /// /// 证书模板管理服务 /// public class CertificateTemplateService : BaseService, 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, // 自动设置为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; } /// /// 更新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.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); } /// /// 获取模板占位符列表 /// [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.DeleteAsync(x => input.Ids.Contains(x.TemplateId)); // 删除模板 await _templateRepository.DeleteAsync(x => input.Ids.Contains(x.Id)); _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.DeleteAsync(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 }; } /// /// 获取当前用户ID /// private long GetCurrentUserUserId() { // 这里需要根据实际的认证机制获取当前用户ID // 暂时返回1,实际项目中需要实现 return 1; } }