diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/AssessmentRecord.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/AssessmentRecord.cs
index f19ccc1..b47005f 100644
--- a/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/AssessmentRecord.cs
+++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/AssessmentRecord.cs
@@ -116,6 +116,12 @@ public class AssessmentRecord
///
public bool IsDeleted { get; set; }
+ ///
+ /// 报告PDF地址
+ ///
+ [MaxLength(500)]
+ public string? ReportUrl { get; set; }
+
///
/// 关联的用户
///
diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordDto.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordDto.cs
index a0ba557..bdf19de 100644
--- a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordDto.cs
+++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordDto.cs
@@ -119,4 +119,9 @@ public class AssessmentRecordDto
/// 创建时间
///
public DateTime CreateTime { get; set; }
+
+ ///
+ /// 报告PDF地址
+ ///
+ public string? ReportUrl { get; set; }
}
diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/AssessmentRecordService.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/AssessmentRecordService.cs
index f2c9a07..68fed24 100644
--- a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/AssessmentRecordService.cs
+++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/AssessmentRecordService.cs
@@ -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();
diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/assessmentRecord.ts b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/assessmentRecord.ts
index 454de53..f19fabd 100644
--- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/assessmentRecord.ts
+++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/api/business/assessmentRecord.ts
@@ -33,6 +33,7 @@ export interface AssessmentRecordItem {
submitTime: string | null
completeTime: string | null
createTime: string
+ reportUrl: string | null
}
/** 答案详情 */
diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/assessment/record/index.vue b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/assessment/record/index.vue
index e3bfd2e..64b026b 100644
--- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/assessment/record/index.vue
+++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/assessment/record/index.vue
@@ -108,7 +108,7 @@
-
+
@@ -134,6 +134,26 @@
网页报告
+
+
+ 查看PDF
+
+
+
+ 下载PDF
+
+
+
diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/PdfGenerationService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/PdfGenerationService.cs
index 9424f92..60c1875 100644
--- a/server/MiAssessment/src/MiAssessment.Core/Services/PdfGenerationService.cs
+++ b/server/MiAssessment/src/MiAssessment.Core/Services/PdfGenerationService.cs
@@ -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 _logger;
+ private const string ReportBasePath = "reports";
+ private const string UploadConfigKey = "uploads";
+
+ private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
+
///
/// 初始化 PDF 报告生成服务
///
- /// 数据库上下文
+ /// 业务数据库上下文
+ /// Admin 配置只读上下文
/// 截图服务
/// 报告生成配置
/// 应用程序配置
/// 日志记录器
public PdfGenerationService(
MiAssessmentDbContext dbContext,
+ AdminConfigReadDbContext adminConfigDbContext,
IScreenshotService screenshotService,
ReportSettings reportSettings,
AppSettings appSettings,
ILogger 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);
}
///
@@ -262,6 +301,104 @@ public class PdfGenerationService : IPdfGenerationService
return protocol + path;
}
+ ///
+ /// 上传 PDF 文件到腾讯云 COS
+ ///
+ /// 本地文件路径
+ /// 文件名
+ /// COS 访问 URL,失败返回 null
+ private async Task 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(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;
+ }
+ }
+
///
/// 使用 PdfSharpCore 将图片列表合并为 PDF 文件并保存
///
diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs
index 72037f5..837f4f1 100644
--- a/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs
+++ b/server/MiAssessment/src/MiAssessment.Core/Services/ReportGenerationService.cs
@@ -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(生成中)
///
/// 测评记录ID
@@ -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();
diff --git a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs
index d874ef4..29d24f7 100644
--- a/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs
+++ b/server/MiAssessment/src/MiAssessment.Infrastructure/Modules/ServiceModule.cs
@@ -161,11 +161,12 @@ public class ServiceModule : Module
builder.Register(c =>
{
var dbContext = c.Resolve();
+ var adminConfigDbContext = c.Resolve();
var screenshotService = c.Resolve();
var reportSettings = c.Resolve();
var appSettings = c.Resolve();
var logger = c.Resolve>();
- return new PdfGenerationService(dbContext, screenshotService, reportSettings, appSettings, logger);
+ return new PdfGenerationService(dbContext, adminConfigDbContext, screenshotService, reportSettings, appSettings, logger);
}).As().InstancePerLifetimeScope();
// ========== 小程序测评模块服务注册 ==========