feat(assessment): Add retest status for equal score detection
Some checks failed
continuous-integration/drone/push Build is failing

- Add new assessment status 7 ("需重测") for cases where all scores are equal
- Create AllScoresEqualException to handle scenarios where 8 intelligences or 40 ability dimensions have identical scores
- Implement CheckAllScoresEqual validation in ReportGenerationService to detect and prevent invalid report generation
- Add UpdateRecordStatusToRetestAsync method in ReportQueueConsumer to handle retest status updates
- Update admin UI status tag mapping to display retest status with warning indicator
- Add user-friendly message for retest status in AssessmentService
- Update status description mappings across services to include new retest status
- Prevent PDF generation when all scores are equal, prompting users to retake the assessment
This commit is contained in:
zpc 2026-03-31 15:08:23 +08:00
parent 04b9fa8220
commit f8a9aaf71f
10 changed files with 269 additions and 9 deletions

View File

@ -34,7 +34,8 @@ public class AssessmentRecordService : IAssessmentRecordService
{ 3, "生成中" }, { 3, "生成中" },
{ 4, "已完成" }, { 4, "已完成" },
{ 5, "生成失败" }, { 5, "生成失败" },
{ 6, "数据已就绪" } { 6, "数据已就绪" },
{ 7, "需重测" }
}; };
/// <summary> /// <summary>

View File

@ -495,6 +495,7 @@ function getStatusTagType(status: number): 'info' | 'primary' | 'warning' | 'suc
case 4: return 'success' case 4: return 'success'
case 5: return 'danger' case 5: return 'danger'
case 6: return 'primary' case 6: return 'primary'
case 7: return 'warning'
default: return 'info' default: return 'info'
} }
} }

View File

@ -1,4 +1,5 @@
using System.Text.Json; using System.Text.Json;
using MiAssessment.Core.Exceptions;
using MiAssessment.Core.Interfaces; using MiAssessment.Core.Interfaces;
using MiAssessment.Core.Models; using MiAssessment.Core.Models;
using MiAssessment.Core.Services; using MiAssessment.Core.Services;
@ -165,6 +166,13 @@ public class ReportQueueConsumer : BackgroundService
await UpdateRecordStatusToFailedAsync(message.RecordId); await UpdateRecordStatusToFailedAsync(message.RecordId);
} }
} }
catch (AllScoresEqualException asEx)
{
// 同分异常不重试直接设置状态为需重测Status=7
_logger.LogWarning("检测到全同分情况RecordId: {RecordId}, ScoreType: {ScoreType}, Message: {Message}",
message.RecordId, asEx.ScoreType, asEx.Message);
await UpdateRecordStatusToRetestAsync(message.RecordId);
}
catch (Exception ex) catch (Exception ex)
{ {
// 报告生成失败,执行重试或死信逻辑 // 报告生成失败,执行重试或死信逻辑
@ -257,4 +265,41 @@ public class ReportQueueConsumer : BackgroundService
_logger.LogError(ex, "更新测评记录状态为生成失败时发生异常RecordId: {RecordId}", recordId); _logger.LogError(ex, "更新测评记录状态为生成失败时发生异常RecordId: {RecordId}", recordId);
} }
} }
/// <summary>
/// 更新测评记录状态为需重测Status=7
/// </summary>
/// <remarks>
/// 当检测到八大智能8项全同分或40项细分维度全同分时调用
/// 不生成PDF提示用户重新测评。
/// </remarks>
/// <param name="recordId">测评记录ID</param>
private async Task UpdateRecordStatusToRetestAsync(long recordId)
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<MiAssessmentDbContext>();
var record = await dbContext.AssessmentRecords
.FirstOrDefaultAsync(r => r.Id == recordId);
if (record != null)
{
record.Status = 7;
record.UpdateTime = DateTime.Now;
await dbContext.SaveChangesAsync();
_logger.LogInformation("测评记录状态已更新为需重测RecordId: {RecordId}", recordId);
}
else
{
_logger.LogWarning("更新状态失败测评记录不存在RecordId: {RecordId}", recordId);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "更新测评记录状态为需重测时发生异常RecordId: {RecordId}", recordId);
}
}
} }

View File

@ -0,0 +1,27 @@
namespace MiAssessment.Core.Exceptions;
/// <summary>
/// 所有分数相同异常八大智能全同分或40项细分全同分
/// </summary>
/// <remarks>
/// 当检测到八大智能8项全部同分或40项细分维度全部同分时抛出
/// 表示无法生成有效的差异化报告,需要用户重新测评。
/// </remarks>
public class AllScoresEqualException : Exception
{
/// <summary>
/// 同分类型Intelligence=八大智能同分Ability=细分维度同分
/// </summary>
public string ScoreType { get; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="scoreType">同分类型</param>
/// <param name="message">异常信息</param>
public AllScoresEqualException(string scoreType, string message)
: base(message)
{
ScoreType = scoreType;
}
}

View File

@ -119,6 +119,11 @@ public class AssessmentService : IAssessmentService
{ {
record.Message = "报告生成失败,请联系客服"; record.Message = "报告生成失败,请联系客服";
} }
// 对需重测状态设置提示信息
else if (record.Status == 7)
{
record.Message = "分析得出多个智能处于同一梯队,我们需要更细致的分析维度,接下来请您重新进行测评";
}
_logger.LogDebug("查询报告状态成功status: {Status}, isCompleted: {IsCompleted}", record.Status, record.IsCompleted); _logger.LogDebug("查询报告状态成功status: {Status}, isCompleted: {IsCompleted}", record.Status, record.IsCompleted);
} }
@ -655,6 +660,7 @@ public class AssessmentService : IAssessmentService
4 => "已完成", 4 => "已完成",
5 => "生成失败", 5 => "生成失败",
6 => "报告生成中", 6 => "报告生成中",
7 => "需重测",
_ => "未知" _ => "未知"
}; };
} }

View File

@ -1,3 +1,4 @@
using MiAssessment.Core.Exceptions;
using MiAssessment.Model.Data; using MiAssessment.Model.Data;
using MiAssessment.Model.Entities; using MiAssessment.Model.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -60,6 +61,9 @@ public class ReportGenerationService
_logger.LogDebug("父级汇总完成recordId: {RecordId}, 新增父级数量: {Count}, 总数量: {Total}", _logger.LogDebug("父级汇总完成recordId: {RecordId}, 新增父级数量: {Count}, 总数量: {Total}",
recordId, parentScores.Count, categoryScores.Count); recordId, parentScores.Count, categoryScores.Count);
// 步骤3.6:检测同分情况(八大智能全同分或细分维度全同分)
CheckAllScoresEqual(categoryScores);
// 步骤4按 CategoryType 分组排名 // 步骤4按 CategoryType 分组排名
var rankedScores = CalculateRanks(categoryScores); var rankedScores = CalculateRanks(categoryScores);
@ -300,6 +304,39 @@ public class ReportGenerationService
return parentScores; return parentScores;
} }
/// <summary>
/// 检测同分情况八大智能8项全同分或细分维度全同分
/// </summary>
/// <param name="categoryScores">所有分类得分(含叶子和父级)</param>
/// <exception cref="AllScoresEqualException">当检测到全同分时抛出</exception>
internal static void CheckAllScoresEqual(List<CategoryScore> categoryScores)
{
// CategoryType=1 为八大智能(父级汇总后的得分)
var intelligenceScores = categoryScores
.Where(s => s.CategoryType == 1)
.Select(s => s.Percentage)
.ToList();
if (intelligenceScores.Count >= 8 && intelligenceScores.Distinct().Count() == 1)
{
throw new AllScoresEqualException("Intelligence",
$"八大智能8项得分全部相同{intelligenceScores.First()}%),无法生成差异化报告");
}
// CategoryType=3 为细分能力维度
var abilityScores = categoryScores
.Where(s => s.CategoryType == 3)
.Select(s => s.Percentage)
.ToList();
if (abilityScores.Count >= 40 && abilityScores.Distinct().Count() == 1)
{
throw new AllScoresEqualException("Ability",
$"40项细分维度得分全部相同{abilityScores.First()}%),无法生成差异化报告");
}
}
/// <summary> /// <summary>
/// 分类得分计算结果 /// 分类得分计算结果
/// </summary> /// </summary>

View File

@ -6,7 +6,7 @@ namespace MiAssessment.Model.Models.Assessment;
public class ResultStatusDto public class ResultStatusDto
{ {
/// <summary> /// <summary>
/// 状态0待支付 1待测评 2测评中 3生成中 4已完成 5生成失败 /// 状态0待支付 1待测评 2测评中 3生成中 4已完成 5生成失败 6数据已就绪 7需重测
/// </summary> /// </summary>
public int Status { get; set; } public int Status { get; set; }

View File

@ -21,7 +21,8 @@ const ASSESSMENT_STATUS = {
GENERATING: 3, // GENERATING: 3, //
COMPLETED: 4, // COMPLETED: 4, //
FAILED: 5, // FAILED: 5, //
DATA_READY: 6 // PDF DATA_READY: 6, // PDF
NEED_RETEST: 7 //
} }
// //
@ -46,7 +47,8 @@ function getStatusClass(status) {
[ASSESSMENT_STATUS.GENERATING]: 'status-generating', [ASSESSMENT_STATUS.GENERATING]: 'status-generating',
[ASSESSMENT_STATUS.COMPLETED]: 'status-completed', [ASSESSMENT_STATUS.COMPLETED]: 'status-completed',
[ASSESSMENT_STATUS.FAILED]: 'status-failed', [ASSESSMENT_STATUS.FAILED]: 'status-failed',
[ASSESSMENT_STATUS.DATA_READY]: 'status-generating' [ASSESSMENT_STATUS.DATA_READY]: 'status-generating',
[ASSESSMENT_STATUS.NEED_RETEST]: 'status-retest'
} }
return classMap[status] || '' return classMap[status] || ''
} }
@ -155,6 +157,14 @@ function viewResult(record) {
}) })
return return
} }
// -
if (record.status === ASSESSMENT_STATUS.NEED_RETEST) {
uni.navigateTo({
url: `/pages/assessment/info/index?typeId=${record.assessmentTypeId || 1}`
})
return
}
} }
/** /**
@ -208,13 +218,19 @@ onMounted(() => {
</view> </view>
</view> </view>
<!-- 卡片底部已完成显示查看报告 --> <!-- 卡片底部已完成显示查看报告需重测显示重新测试 -->
<view class="card-footer" v-if="record.status === ASSESSMENT_STATUS.COMPLETED || record.status === ASSESSMENT_STATUS.DATA_READY"> <view class="card-footer" v-if="record.status === ASSESSMENT_STATUS.COMPLETED || record.status === ASSESSMENT_STATUS.DATA_READY">
<view class="view-btn"> <view class="view-btn">
<text>查看报告</text> <text>查看报告</text>
<view class="arrow-icon"></view> <view class="arrow-icon"></view>
</view> </view>
</view> </view>
<view class="card-footer" v-else-if="record.status === ASSESSMENT_STATUS.NEED_RETEST">
<view class="view-btn retest-btn">
<text>重新测试</text>
<view class="arrow-icon"></view>
</view>
</view>
</view> </view>
<!-- 加载更多 --> <!-- 加载更多 -->
@ -296,6 +312,18 @@ onMounted(() => {
color: $success-color; color: $success-color;
background-color: rgba(82, 196, 26, 0.1); background-color: rgba(82, 196, 26, 0.1);
} }
// -
&.status-failed {
color: $error-color;
background-color: rgba(255, 77, 79, 0.1);
}
// -
&.status-retest {
color: $warning-color;
background-color: rgba(250, 173, 20, 0.1);
}
} }
} }
@ -331,6 +359,15 @@ onMounted(() => {
align-items: center; align-items: center;
color: $primary-color; color: $primary-color;
font-size: $font-size-md; font-size: $font-size-md;
&.retest-btn {
color: $warning-color;
.arrow-icon {
border-right-color: $warning-color;
border-bottom-color: $warning-color;
}
}
.arrow-icon { .arrow-icon {
width: 12rpx; width: 12rpx;

View File

@ -7,6 +7,7 @@
* - 显示提示文字 * - 显示提示文字
* - 轮询查询报告生成状态3秒间隔 * - 轮询查询报告生成状态3秒间隔
* - 生成完成自动跳转结果页 * - 生成完成自动跳转结果页
* - Status=7 需重测显示提示和重新测试按钮
*/ */
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
@ -19,6 +20,7 @@ const userStore = useUserStore()
// //
const recordId = ref('') const recordId = ref('')
const typeId = ref('')
// //
let pollTimer = null let pollTimer = null
@ -32,6 +34,10 @@ const MAX_POLL_COUNT = 100
// //
const pollCount = ref(0) const pollCount = ref(0)
// Status=7
const needRetest = ref(false)
const retestMessage = ref('')
/** /**
* 开始轮询查询状态 * 开始轮询查询状态
*/ */
@ -90,7 +96,7 @@ async function checkStatus() {
if (res && res.code === 0 && res.data) { if (res && res.code === 0 && res.data) {
const status = res.data.status const status = res.data.status
// 3- 4- 5- // 3- 4- 5- 7-
if (status === 4) { if (status === 4) {
// //
stopPolling() stopPolling()
@ -100,6 +106,11 @@ async function checkStatus() {
uni.redirectTo({ uni.redirectTo({
url: `/pages/assessment/result/index?recordId=${recordId.value}&reportUrl=${encodeURIComponent(reportUrl)}` url: `/pages/assessment/result/index?recordId=${recordId.value}&reportUrl=${encodeURIComponent(reportUrl)}`
}) })
} else if (status === 7) {
//
stopPolling()
needRetest.value = true
retestMessage.value = res.data.message || '分析得出多个智能处于同一梯队,我们需要更细致的分析维度,接下来请您重新进行测评'
} else if (status === 5) { } else if (status === 5) {
// //
stopPolling() stopPolling()
@ -115,7 +126,7 @@ async function checkStatus() {
} }
}) })
} }
// status === 3 // status === 3 6
} }
} catch (error) { } catch (error) {
console.error('查询状态失败:', error) console.error('查询状态失败:', error)
@ -123,11 +134,28 @@ async function checkStatus() {
} }
} }
/**
* 重新测试
*/
function handleRetest() {
//
if (typeId.value) {
uni.redirectTo({
url: `/pages/assessment/info/index?typeId=${typeId.value}`
})
} else {
uni.switchTab({
url: '/pages/index/index'
})
}
}
/** /**
* 页面加载 * 页面加载
*/ */
onLoad((options) => { onLoad((options) => {
recordId.value = options.recordId || '' recordId.value = options.recordId || ''
typeId.value = options.typeId || ''
// //
userStore.restoreFromStorage() userStore.restoreFromStorage()
@ -149,8 +177,17 @@ onUnmounted(() => {
<!-- 导航栏 --> <!-- 导航栏 -->
<Navbar title="多元智能测评" :showBack="true" /> <Navbar title="多元智能测评" :showBack="true" />
<!-- 需重测提示 -->
<view v-if="needRetest" class="retest-content">
<image class="retest-image" src="/static/cepingzhong.png" mode="aspectFit" />
<view class="retest-text">
<text class="retest-title">{{ retestMessage }}</text>
</view>
</view>
<!-- 加载内容区域 --> <!-- 加载内容区域 -->
<view class="loading-content"> <view v-else class="loading-content">
<!-- 加载图片 --> <!-- 加载图片 -->
<image class="loading-image" src="/static/cepingzhong.png" mode="aspectFit" /> <image class="loading-image" src="/static/cepingzhong.png" mode="aspectFit" />
@ -160,6 +197,13 @@ onUnmounted(() => {
<text class="loading-tip">可在往期测评中查看测评结果</text> <text class="loading-tip">可在往期测评中查看测评结果</text>
</view> </view>
</view> </view>
<!-- 重新测试按钮 -->
<view v-if="needRetest" class="bottom-action">
<view class="retest-btn" @click="handleRetest">
<text>重新测试</text>
</view>
</view>
</view> </view>
</template> </template>
@ -170,6 +214,8 @@ onUnmounted(() => {
.assessment-loading-page { .assessment-loading-page {
min-height: 100vh; min-height: 100vh;
background-color: $bg-white; background-color: $bg-white;
display: flex;
flex-direction: column;
} }
.loading-content { .loading-content {
@ -205,4 +251,64 @@ onUnmounted(() => {
font-size: $font-size-md; font-size: $font-size-md;
color: $text-placeholder; color: $text-placeholder;
} }
//
.retest-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $spacing-xl 64rpx;
}
.retest-image {
width: 360rpx;
height: 360rpx;
margin-bottom: $spacing-xl;
}
.retest-text {
text-align: center;
}
.retest-title {
display: block;
font-size: $font-size-lg;
color: $text-secondary;
line-height: 1.6;
}
//
.bottom-action {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: $spacing-lg $spacing-xl;
padding-bottom: calc(#{$spacing-lg} + env(safe-area-inset-bottom));
background-color: transparent;
z-index: 100;
}
//
.retest-btn {
width: 100%;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FF8A6B 0%, #FF6B6B 100%);
border-radius: $border-radius-round;
text {
font-size: $font-size-lg;
color: $text-white;
font-weight: $font-weight-medium;
}
&:active {
opacity: 0.85;
}
}
</style> </style>

View File

@ -198,7 +198,7 @@ async function handleSubmit() {
if (res && res.code === 0) { if (res && res.code === 0) {
const resRecordId = res.data?.recordId || res.data?.id || recordId.value const resRecordId = res.data?.recordId || res.data?.id || recordId.value
uni.redirectTo({ url: `/pages/assessment/loading/index?recordId=${resRecordId}` }) uni.redirectTo({ url: `/pages/assessment/loading/index?recordId=${resRecordId}&typeId=${typeId.value}` })
} else { } else {
uni.showToast({ title: res?.message || '提交失败,请重试', icon: 'none' }) uni.showToast({ title: res?.message || '提交失败,请重试', icon: 'none' })
} }