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, "生成中" },
{ 4, "已完成" },
{ 5, "生成失败" },
{ 6, "数据已就绪" }
{ 6, "数据已就绪" },
{ 7, "需重测" }
};
/// <summary>

View File

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

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using MiAssessment.Core.Exceptions;
using MiAssessment.Core.Interfaces;
using MiAssessment.Core.Models;
using MiAssessment.Core.Services;
@ -165,6 +166,13 @@ public class ReportQueueConsumer : BackgroundService
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)
{
// 报告生成失败,执行重试或死信逻辑
@ -257,4 +265,41 @@ public class ReportQueueConsumer : BackgroundService
_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 = "报告生成失败,请联系客服";
}
// 对需重测状态设置提示信息
else if (record.Status == 7)
{
record.Message = "分析得出多个智能处于同一梯队,我们需要更细致的分析维度,接下来请您重新进行测评";
}
_logger.LogDebug("查询报告状态成功status: {Status}, isCompleted: {IsCompleted}", record.Status, record.IsCompleted);
}
@ -655,6 +660,7 @@ public class AssessmentService : IAssessmentService
4 => "已完成",
5 => "生成失败",
6 => "报告生成中",
7 => "需重测",
_ => "未知"
};
}

View File

@ -1,3 +1,4 @@
using MiAssessment.Core.Exceptions;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using Microsoft.EntityFrameworkCore;
@ -60,6 +61,9 @@ public class ReportGenerationService
_logger.LogDebug("父级汇总完成recordId: {RecordId}, 新增父级数量: {Count}, 总数量: {Total}",
recordId, parentScores.Count, categoryScores.Count);
// 步骤3.6:检测同分情况(八大智能全同分或细分维度全同分)
CheckAllScoresEqual(categoryScores);
// 步骤4按 CategoryType 分组排名
var rankedScores = CalculateRanks(categoryScores);
@ -300,6 +304,39 @@ public class ReportGenerationService
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>

View File

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

View File

@ -21,7 +21,8 @@ const ASSESSMENT_STATUS = {
GENERATING: 3, //
COMPLETED: 4, //
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.COMPLETED]: 'status-completed',
[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] || ''
}
@ -155,6 +157,14 @@ function viewResult(record) {
})
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 class="card-footer" v-if="record.status === ASSESSMENT_STATUS.COMPLETED || record.status === ASSESSMENT_STATUS.DATA_READY">
<view class="view-btn">
<text>查看报告</text>
<view class="arrow-icon"></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>
<!-- 加载更多 -->
@ -296,6 +312,18 @@ onMounted(() => {
color: $success-color;
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;
color: $primary-color;
font-size: $font-size-md;
&.retest-btn {
color: $warning-color;
.arrow-icon {
border-right-color: $warning-color;
border-bottom-color: $warning-color;
}
}
.arrow-icon {
width: 12rpx;

View File

@ -7,6 +7,7 @@
* - 显示提示文字
* - 轮询查询报告生成状态3秒间隔
* - 生成完成自动跳转结果页
* - Status=7 需重测显示提示和重新测试按钮
*/
import { ref, onMounted, onUnmounted } from 'vue'
@ -19,6 +20,7 @@ const userStore = useUserStore()
//
const recordId = ref('')
const typeId = ref('')
//
let pollTimer = null
@ -32,6 +34,10 @@ const MAX_POLL_COUNT = 100
//
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) {
const status = res.data.status
// 3- 4- 5-
// 3- 4- 5- 7-
if (status === 4) {
//
stopPolling()
@ -100,6 +106,11 @@ async function checkStatus() {
uni.redirectTo({
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) {
//
stopPolling()
@ -115,7 +126,7 @@ async function checkStatus() {
}
})
}
// status === 3
// status === 3 6
}
} catch (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) => {
recordId.value = options.recordId || ''
typeId.value = options.typeId || ''
//
userStore.restoreFromStorage()
@ -149,8 +177,17 @@ onUnmounted(() => {
<!-- 导航栏 -->
<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" />
@ -160,6 +197,13 @@ onUnmounted(() => {
<text class="loading-tip">可在往期测评中查看测评结果</text>
</view>
</view>
<!-- 重新测试按钮 -->
<view v-if="needRetest" class="bottom-action">
<view class="retest-btn" @click="handleRetest">
<text>重新测试</text>
</view>
</view>
</view>
</template>
@ -170,6 +214,8 @@ onUnmounted(() => {
.assessment-loading-page {
min-height: 100vh;
background-color: $bg-white;
display: flex;
flex-direction: column;
}
.loading-content {
@ -205,4 +251,64 @@ onUnmounted(() => {
font-size: $font-size-md;
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>

View File

@ -198,7 +198,7 @@ async function handleSubmit() {
if (res && res.code === 0) {
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 {
uni.showToast({ title: res?.message || '提交失败,请重试', icon: 'none' })
}