This commit is contained in:
18631081161 2026-04-02 18:03:38 +08:00
commit feb181a130
17 changed files with 364 additions and 71 deletions

View File

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

View File

@ -106,10 +106,14 @@
<el-table-column prop="orderNo" label="订单号" width="170" show-overflow-tooltip />
<el-table-column prop="submitTime" label="提交时间" width="170" align="center">
<template #default="{ row }">
{{ row.submitTime || '-' }}
{{ formatDateTime(row.submitTime) }}
</template>
</el-table-column>
<el-table-column label="创建时间" width="170" align="center">
<template #default="{ row }">
{{ formatDateTime(row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" 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)">
@ -219,10 +223,10 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="订单号" :span="2">{{ state.detail.orderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ state.detail.startTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ state.detail.submitTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ state.detail.completeTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ state.detail.createTime }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ formatDateTime(state.detail.startTime) }}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{ formatDateTime(state.detail.submitTime) }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ formatDateTime(state.detail.completeTime) }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDateTime(state.detail.createTime) }}</el-descriptions-item>
</el-descriptions>
</div>
@ -482,6 +486,16 @@ const editConclusionDialogVisible = computed({
// ============ Helper Functions ============
/**
* 格式化时间戳去掉T秒数不带小数点
* 2026-03-31T07:50:35.7396316 2026-03-31 07:50:35
*/
function formatDateTime(val: string | null | undefined): string {
if (!val) return '-'
// 19T
return val.substring(0, 19).replace('T', ' ')
}
/**
* 获取状态标签类型
* 待支付=info, 待测评=info, 测评中=primary, 生成中=warning, 已完成=success
@ -495,6 +509,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

@ -14,5 +14,7 @@ RUN dotnet publish "src/MiAssessment.Api/MiAssessment.Api.csproj" -c Release -o
FROM base AS final
WORKDIR /app
# 安装字体(页码渲染需要)
RUN apt-get update && apt-get install -y --no-install-recommends fonts-dejavu-core && rm -rf /var/lib/apt/lists/*
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MiAssessment.Api.dll"]

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);
}
@ -218,8 +223,8 @@ public class AssessmentService : IAssessmentService
throw new UnauthorizedAccessException("无权限访问该测评记录");
}
// 验证记录状态(只有待测评或测评中状态才能提交答案)
if (record.Status != 1 && record.Status != 2)
// 验证记录状态(只有待测评、测评中或需重测状态才能提交答案)
if (record.Status != 1 && record.Status != 2 && record.Status != 7)
{
_logger.LogWarning("测评记录状态不允许提交答案status: {Status}, recordId: {RecordId}",
record.Status, request.RecordId);
@ -655,6 +660,7 @@ public class AssessmentService : IAssessmentService
4 => "已完成",
5 => "生成失败",
6 => "报告生成中",
7 => "需重测",
_ => "未知"
};
}
@ -689,7 +695,7 @@ public class AssessmentService : IAssessmentService
.AsNoTracking()
.Where(r => r.UserId == userId
&& r.AssessmentTypeId == typeId
&& (r.Status == 1 || r.Status == 2)
&& (r.Status == 1 || r.Status == 2 || r.Status == 7)
&& !r.IsDeleted)
.OrderByDescending(r => r.CreateTime)
.Select(r => new PendingRecordDto

View File

@ -904,6 +904,7 @@ public class OrderService : IOrderService
4 => "已测评",
5 => "生成失败",
6 => "报告生成中",
7 => "待测评",
_ => GetOrderStatusText(orderStatus)
};
}

View File

@ -449,17 +449,27 @@ public class PdfGenerationService : IPdfGenerationService
private static void BuildAndSavePdf(List<byte[]> images, string filePath)
{
using var document = new PdfDocument();
var totalPages = images.Count;
var font = new XFont("DejaVu Sans", 10, XFontStyle.Regular);
var brush = new XSolidBrush(XColor.FromArgb(153, 153, 153)); // #999999
foreach (var imageBytes in images)
for (var i = 0; i < totalPages; i++)
{
var imageBytes = images[i];
var page = document.AddPage();
page.Width = XUnit.FromPoint(PageWidthPt);
page.Height = XUnit.FromPoint(PageHeightPt);
using var stream = new MemoryStream(imageBytes);
using var xImage = XImage.FromStream(() => new MemoryStream(imageBytes));
using var gfx = XGraphics.FromPdfPage(page);
gfx.DrawImage(xImage, 0, 0, page.Width, page.Height);
// 绘制页码(底部居中)
var pageNumber = $"{i + 1} / {totalPages}";
var size = gfx.MeasureString(pageNumber, font);
var x = (page.Width - size.Width) / 2;
var y = page.Height - 20; // 距底部 20pt
gfx.DrawString(pageNumber, font, brush, x, y);
}
document.Save(filePath);

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

@ -1,6 +1,6 @@
{
"name" : "学业邑规划",
"appid" : "__UNI__A612028",
"appid" : "__UNI__1BAACAB",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",

View File

@ -12,7 +12,7 @@
"path": "pages/team/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "团队"
"navigationBarTitleText": "产品"
}
},
{
@ -147,7 +147,7 @@
},
{
"pagePath": "pages/team/index",
"text": "团队",
"text": "产品",
"iconPath": "static/tabbar/message.png",
"selectedIconPath": "static/tabbar/message_s.png"
},

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

@ -584,53 +584,59 @@
</view>
</view>
<!-- 邀请码弹窗 -->
<view v-if="showInvitePopup" class="popup-mask" @click="closeInvitePopup">
<view class="popup-container" @click.stop>
<view class="popup-header">
<text class="popup-title">填写测评邀请码</text>
<view class="popup-close" @click="closeInvitePopup">
<text>×</text>
</view>
</view>
<view class="popup-body">
<input class="invite-input" type="text" placeholder="请输入5位邀请码"
placeholder-style="letter-spacing: 0rpx;" :value="inviteCode" @input="onInviteCodeInput"
maxlength="5" />
</view>
<view class="popup-footer">
<view class="popup-btn" :class="{ 'btn-loading': inviteLoading }" @click="submitInviteCode">
<text>{{ inviteLoading ? '验证中...' : '提交' }}</text>
</view>
</view>
</view>
</view>
<!-- 进行中测评弹窗 -->
<view v-if="showPendingPopup" class="popup-mask" @click="handleDismissPending">
<view class="popup-container pending-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">发现进行中的测评</text>
</view>
<view class="popup-body">
<view class="pending-msg">
您有一份未完成的测评记录是否继续
</view>
<view class="pending-info">
<text>姓名{{ pendingRecord?.name }}</text>
<text>手机号{{ pendingRecord?.phone }}</text>
</view>
</view>
<view class="pending-footer">
<view class="popup-btn-outline" @click="handleDismissPending">
<text>重新开始</text>
</view>
<view class="popup-btn" @click="handleContinuePending">
<text>继续测评</text>
</view>
</view>
</view>
</view>
</view>
<!-- 邀请码弹窗 -->
<view v-if="showInvitePopup" class="popup-mask" @click="closeInvitePopup">
<view class="popup-container" @click.stop>
<view class="popup-header">
<text class="popup-title">填写测评邀请码</text>
<view class="popup-close" @click="closeInvitePopup">
<text>×</text>
</view>
</view>
<view class="popup-body">
<input
class="invite-input"
type="text"
placeholder="请输入5位邀请码"
placeholder-style="letter-spacing: 0rpx;"
:value="inviteCode"
@input="onInviteCodeInput"
maxlength="5"
/>
</view>
<view class="popup-footer">
<view class="popup-btn" :class="{ 'btn-loading': inviteLoading }" @click="submitInviteCode">
<text>{{ inviteLoading ? '验证中...' : '提交' }}</text>
</view>
</view>
</view>
</view>
<!-- 进行中测评弹窗 -->
<view v-if="showPendingPopup" class="popup-mask" @click="handleDismissPending">
<view class="popup-container pending-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">发现进行中的测评</text>
</view>
<view class="popup-body">
<view class="pending-msg">
您有一份未完成的测评记录是否继续
</view>
<view class="pending-info">
<text>姓名{{ pendingRecord?.name }}</text>
<text>手机号{{ pendingRecord?.phone }}</text>
</view>
</view>
<view class="pending-footer">
<view class="popup-btn-outline" @click="handleDismissPending">
<text>重新开始</text>
</view>
<view class="popup-btn" @click="handleContinuePending">
<text>继续测评</text>
</view>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>

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' })
}

View File

@ -58,7 +58,7 @@ function getDisplayStatus(order) {
// /
if (assessmentStatus) {
const map = { 1: '待测评', 2: '测评中', 3: '测评生成中', 4: '已测评' }
const map = { 1: '待测评', 2: '测评中', 3: '测评生成中', 4: '已测评', 7: '待测评' }
return map[assessmentStatus] || order.statusText || '已支付'
}