feat(report): PDF生成后才标记已完成,后台增加PDF查看下载
- ReportGenerationService: 结论持久化后保持Status=3,不再设置Status=4 - PdfGenerationService: PDF生成+COS上传成功后设置Status=4和CompleteTime - ReportQueueConsumer: PDF生成失败时更新Status=5(生成失败) - AssessmentRecordDto: 增加ReportUrl字段 - AssessmentRecordService: 列表查询增加ReportUrl映射 - Admin.Business实体: AssessmentRecord增加ReportUrl属性 - 前端API类型: AssessmentRecordItem增加reportUrl字段 - 后台记录页面: 增加查看PDF和下载PDF按钮 - Core项目: 增加Tencent.QCloud.Cos.Sdk依赖,支持COS上传
This commit is contained in:
parent
6d81fa45f4
commit
0b70ef0471
|
|
@ -116,6 +116,12 @@ public class AssessmentRecord
|
|||
/// </summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 报告PDF地址
|
||||
/// </summary>
|
||||
[MaxLength(500)]
|
||||
public string? ReportUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的用户
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -119,4 +119,9 @@ public class AssessmentRecordDto
|
|||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 报告PDF地址
|
||||
/// </summary>
|
||||
public string? ReportUrl { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@ public class AssessmentRecordService : IAssessmentRecordService
|
|||
StartTime = r.StartTime,
|
||||
SubmitTime = r.SubmitTime,
|
||||
CompleteTime = r.CompleteTime,
|
||||
CreateTime = r.CreateTime
|
||||
CreateTime = r.CreateTime,
|
||||
ReportUrl = r.ReportUrl
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface AssessmentRecordItem {
|
|||
submitTime: string | null
|
||||
completeTime: string | null
|
||||
createTime: string
|
||||
reportUrl: string | null
|
||||
}
|
||||
|
||||
/** 答案详情 */
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="380" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="handleViewDetail(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
|
|
@ -134,6 +134,26 @@
|
|||
<el-icon><Monitor /></el-icon>
|
||||
网页报告
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.reportUrl"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleViewPdf(row)"
|
||||
>
|
||||
<el-icon><Document /></el-icon>
|
||||
查看PDF
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.reportUrl"
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDownloadPdf(row)"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
下载PDF
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 3 || row.status === 5"
|
||||
type="warning"
|
||||
|
|
@ -649,6 +669,25 @@ function handleViewWebReport(row: AssessmentRecordItem) {
|
|||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
/** 在新标签页查看 PDF 报告 */
|
||||
function handleViewPdf(row: AssessmentRecordItem) {
|
||||
if (row.reportUrl) {
|
||||
window.open(row.reportUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载 PDF 报告 */
|
||||
function handleDownloadPdf(row: AssessmentRecordItem) {
|
||||
if (!row.reportUrl) return
|
||||
const link = document.createElement('a')
|
||||
link.href = row.reportUrl
|
||||
link.download = `测评报告_${row.name}_${row.id}.pdf`
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
/** 表格选择变化 */
|
||||
function handleSelectionChange(rows: AssessmentRecordItem[]) {
|
||||
state.selectedRows = rows
|
||||
|
|
|
|||
|
|
@ -160,8 +160,9 @@ public class ReportQueueConsumer : BackgroundService
|
|||
}
|
||||
catch (Exception pdfEx)
|
||||
{
|
||||
// PDF 生成失败不影响结论数据,仅记录错误日志
|
||||
// PDF 生成失败,更新状态为生成失败
|
||||
_logger.LogError(pdfEx, "PDF 报告生成失败,RecordId: {RecordId}", message.RecordId);
|
||||
await UpdateRecordStatusToFailedAsync(message.RecordId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<PackageReference Include="PdfSharpCore" Version="1.3.67" />
|
||||
<!-- Tencent Cloud COS SDK -->
|
||||
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.44" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
using System.Text.Json;
|
||||
using COSXML;
|
||||
using COSXML.Auth;
|
||||
using COSXML.Model.Object;
|
||||
using MiAssessment.Core.Interfaces;
|
||||
using MiAssessment.Core.Models;
|
||||
using MiAssessment.Model.Data;
|
||||
using MiAssessment.Model.Entities;
|
||||
using MiAssessment.Model.Models.Auth;
|
||||
using MiAssessment.Model.Models.Config;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using PdfSharpCore.Drawing;
|
||||
|
|
@ -26,27 +31,36 @@ public class PdfGenerationService : IPdfGenerationService
|
|||
private const double PageHeightPt = 694.5;
|
||||
|
||||
private readonly MiAssessmentDbContext _dbContext;
|
||||
private readonly AdminConfigReadDbContext _adminConfigDbContext;
|
||||
private readonly IScreenshotService _screenshotService;
|
||||
private readonly ReportSettings _reportSettings;
|
||||
private readonly AppSettings _appSettings;
|
||||
private readonly ILogger<PdfGenerationService> _logger;
|
||||
|
||||
private const string ReportBasePath = "reports";
|
||||
private const string UploadConfigKey = "uploads";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 PDF 报告生成服务
|
||||
/// </summary>
|
||||
/// <param name="dbContext">数据库上下文</param>
|
||||
/// <param name="dbContext">业务数据库上下文</param>
|
||||
/// <param name="adminConfigDbContext">Admin 配置只读上下文</param>
|
||||
/// <param name="screenshotService">截图服务</param>
|
||||
/// <param name="reportSettings">报告生成配置</param>
|
||||
/// <param name="appSettings">应用程序配置</param>
|
||||
/// <param name="logger">日志记录器</param>
|
||||
public PdfGenerationService(
|
||||
MiAssessmentDbContext dbContext,
|
||||
AdminConfigReadDbContext adminConfigDbContext,
|
||||
IScreenshotService screenshotService,
|
||||
ReportSettings reportSettings,
|
||||
AppSettings appSettings,
|
||||
ILogger<PdfGenerationService> logger)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_adminConfigDbContext = adminConfigDbContext;
|
||||
_screenshotService = screenshotService;
|
||||
_reportSettings = reportSettings;
|
||||
_appSettings = appSettings;
|
||||
|
|
@ -111,21 +125,46 @@ public class PdfGenerationService : IPdfGenerationService
|
|||
|
||||
BuildAndSavePdf(imageList, filePath);
|
||||
|
||||
// 6. 更新数据库 ReportUrl
|
||||
var cdnPrefix = _appSettings.CdnPrefix?.TrimEnd('/') ?? string.Empty;
|
||||
var reportUrl = string.IsNullOrEmpty(cdnPrefix)
|
||||
? $"/reports/{fileName}"
|
||||
: $"{cdnPrefix}/reports/{fileName}";
|
||||
// 6. 尝试上传到 COS
|
||||
string reportUrl;
|
||||
var cosUrl = await UploadToCosAsync(filePath, fileName);
|
||||
if (!string.IsNullOrEmpty(cosUrl))
|
||||
{
|
||||
reportUrl = cosUrl;
|
||||
// COS 上传成功,删除本地文件
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
_logger.LogInformation("本地 PDF 文件已删除: {FilePath}", filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "删除本地 PDF 文件失败: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// COS 上传失败,回退到本地路径
|
||||
var cdnPrefix = _appSettings.CdnPrefix?.TrimEnd('/') ?? string.Empty;
|
||||
reportUrl = string.IsNullOrEmpty(cdnPrefix)
|
||||
? $"/reports/{fileName}"
|
||||
: $"{cdnPrefix}/reports/{fileName}";
|
||||
_logger.LogWarning("COS 上传失败,使用本地路径: {ReportUrl}", reportUrl);
|
||||
}
|
||||
|
||||
// 7. 更新数据库 ReportUrl,设置状态为已完成
|
||||
var record = await _dbContext.AssessmentRecords.FindAsync(recordId);
|
||||
if (record != null)
|
||||
{
|
||||
record.ReportUrl = reportUrl;
|
||||
record.Status = 4;
|
||||
record.CompleteTime = DateTime.Now;
|
||||
record.UpdateTime = DateTime.Now;
|
||||
await _dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
_logger.LogInformation("PDF 报告生成成功,RecordId: {RecordId},文件路径: {FilePath}",
|
||||
recordId, filePath);
|
||||
_logger.LogInformation("PDF 报告生成成功,RecordId: {RecordId},ReportUrl: {ReportUrl}",
|
||||
recordId, reportUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -262,6 +301,104 @@ public class PdfGenerationService : IPdfGenerationService
|
|||
return protocol + path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传 PDF 文件到腾讯云 COS
|
||||
/// </summary>
|
||||
/// <param name="filePath">本地文件路径</param>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <returns>COS 访问 URL,失败返回 null</returns>
|
||||
private async Task<string?> UploadToCosAsync(string filePath, string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从 Admin 库读取上传配置
|
||||
var configValue = await _adminConfigDbContext.AdminConfigs
|
||||
.Where(c => c.ConfigKey == UploadConfigKey)
|
||||
.Select(c => c.ConfigValue)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(configValue))
|
||||
{
|
||||
_logger.LogWarning("未找到上传配置,跳过 COS 上传");
|
||||
return null;
|
||||
}
|
||||
|
||||
var setting = JsonSerializer.Deserialize<UploadSetting>(configValue, JsonOptions);
|
||||
if (setting == null || setting.Type != "3")
|
||||
{
|
||||
_logger.LogWarning("上传配置不是腾讯云 COS 类型(当前: {Type}),跳过 COS 上传", setting?.Type);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证必要配置
|
||||
if (string.IsNullOrWhiteSpace(setting.Bucket) ||
|
||||
string.IsNullOrWhiteSpace(setting.Region) ||
|
||||
string.IsNullOrWhiteSpace(setting.AccessKeyId) ||
|
||||
string.IsNullOrWhiteSpace(setting.AccessKeySecret) ||
|
||||
string.IsNullOrWhiteSpace(setting.Domain))
|
||||
{
|
||||
_logger.LogWarning("COS 配置不完整,跳过上传");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建 COS 对象路径: reports/2026/03/17/report_3_20260317230151.pdf
|
||||
var now = DateTime.Now;
|
||||
var datePath = $"{now.Year}/{now.Month:D2}/{now.Day:D2}";
|
||||
var objectKey = $"{ReportBasePath}/{datePath}/{fileName}";
|
||||
|
||||
// 创建 COS 客户端
|
||||
var cosConfig = new CosXmlConfig.Builder()
|
||||
.IsHttps(true)
|
||||
.SetRegion(setting.Region)
|
||||
.Build();
|
||||
|
||||
var credentialProvider = new DefaultQCloudCredentialProvider(
|
||||
setting.AccessKeyId, setting.AccessKeySecret, 600);
|
||||
|
||||
var cosXml = new CosXmlServer(cosConfig, credentialProvider);
|
||||
|
||||
// 上传文件
|
||||
var fileBytes = await File.ReadAllBytesAsync(filePath);
|
||||
var putRequest = new PutObjectRequest(setting.Bucket, objectKey, fileBytes);
|
||||
putRequest.SetRequestHeader("Content-Type", "application/pdf");
|
||||
|
||||
var result = cosXml.PutObject(putRequest);
|
||||
|
||||
if (result.IsSuccessful())
|
||||
{
|
||||
// 生成访问 URL
|
||||
var domain = setting.Domain!.TrimEnd('/');
|
||||
if (!domain.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
|
||||
!domain.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
domain = $"https://{domain}";
|
||||
}
|
||||
var cosUrl = $"{domain}/{objectKey}";
|
||||
|
||||
_logger.LogInformation("PDF 上传 COS 成功: {CosUrl}", cosUrl);
|
||||
return cosUrl;
|
||||
}
|
||||
|
||||
_logger.LogError("PDF 上传 COS 失败: {HttpMessage}", result.httpMessage);
|
||||
return null;
|
||||
}
|
||||
catch (COSXML.CosException.CosClientException ex)
|
||||
{
|
||||
_logger.LogError(ex, "COS 客户端错误,上传 PDF 失败");
|
||||
return null;
|
||||
}
|
||||
catch (COSXML.CosException.CosServerException ex)
|
||||
{
|
||||
_logger.LogError(ex, "COS 服务端错误,上传 PDF 失败: {Info}", ex.GetInfo());
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "上传 PDF 到 COS 异常");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 PdfSharpCore 将图片列表合并为 PDF 文件并保存
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -515,7 +515,7 @@ public class ReportGenerationService
|
|||
/// - 批量写入新的 AssessmentResult 记录
|
||||
/// - 删除该 RecordId 已有的 AssessmentRecordConclusion 记录(硬删除,支持重新生成)
|
||||
/// - 从 report_conclusions 模板表复制结论数据到 assessment_record_conclusions
|
||||
/// - 更新 AssessmentRecord 的 Status 为 4(已完成)并设置 CompleteTime
|
||||
/// - 保持 AssessmentRecord 的 Status 为 3(生成中),等待 PDF 生成后再更新为 4(已完成)
|
||||
/// 异常时回滚事务,Status 保持为 3(生成中)
|
||||
/// </remarks>
|
||||
/// <param name="recordId">测评记录ID</param>
|
||||
|
|
@ -615,12 +615,10 @@ public class ReportGenerationService
|
|||
_logger.LogDebug("复制结论数据,recordId: {RecordId}, 数量: {Count}", recordId, newConclusions.Count);
|
||||
}
|
||||
|
||||
// 重新加载测评记录(带跟踪),更新状态为4(已完成)
|
||||
// 重新加载测评记录(带跟踪),保持 Status=3(生成中),等待 PDF 生成后再更新为4
|
||||
var record = await _dbContext.AssessmentRecords
|
||||
.FirstAsync(r => r.Id == recordId);
|
||||
|
||||
record.Status = 4;
|
||||
record.CompleteTime = now;
|
||||
record.UpdateTime = now;
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
|
|
|||
|
|
@ -161,11 +161,12 @@ public class ServiceModule : Module
|
|||
builder.Register(c =>
|
||||
{
|
||||
var dbContext = c.Resolve<MiAssessmentDbContext>();
|
||||
var adminConfigDbContext = c.Resolve<AdminConfigReadDbContext>();
|
||||
var screenshotService = c.Resolve<IScreenshotService>();
|
||||
var reportSettings = c.Resolve<Core.Models.ReportSettings>();
|
||||
var appSettings = c.Resolve<AppSettings>();
|
||||
var logger = c.Resolve<ILogger<PdfGenerationService>>();
|
||||
return new PdfGenerationService(dbContext, screenshotService, reportSettings, appSettings, logger);
|
||||
return new PdfGenerationService(dbContext, adminConfigDbContext, screenshotService, reportSettings, appSettings, logger);
|
||||
}).As<IPdfGenerationService>().InstancePerLifetimeScope();
|
||||
|
||||
// ========== 小程序测评模块服务注册 ==========
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user