diff --git a/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs b/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs index 928a0c9..a0fa63c 100644 --- a/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs +++ b/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs @@ -406,4 +406,44 @@ public class AssessmentController : ControllerBase return ApiResponse>.Fail("获取评分标准失败"); } } + + /// + /// 获取用户进行中的测评记录 + /// + /// + /// GET /api/assessment/getPendingRecord?typeId=1 + /// + /// 查询当前用户状态为待测评或测评中的最新测评记录,用于断点续答。 + /// 需要用户登录认证。 + /// + /// 测评类型ID + /// 进行中的测评记录,如果没有则返回null + [HttpGet("getPendingRecord")] + [Authorize] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] + public async Task> GetPendingRecord([FromQuery] long typeId) + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return ApiResponse.Unauthorized(); + } + + try + { + if (typeId <= 0) + { + return ApiResponse.Fail("测评类型ID无效"); + } + + var record = await _assessmentService.GetPendingRecordAsync(userId.Value, typeId); + return ApiResponse.Success(record!); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get pending record, userId: {UserId}, typeId: {TypeId}", userId, typeId); + return ApiResponse.Fail("查询进行中的测评记录失败"); + } + } } diff --git a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs index ab8c3e6..f639b64 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs @@ -112,4 +112,15 @@ public interface IAssessmentService /// 测评类型ID /// 评分标准选项列表 Task> GetScoreOptionsAsync(long typeId); + /// + /// 获取用户进行中的测评记录 + /// + /// + /// 查询当前用户状态为待测评(1)或测评中(2)的最新测评记录。 + /// 用于页面加载时检测是否有未完成的测评,支持断点续答。 + /// + /// 当前用户ID + /// 测评类型ID + /// 进行中的测评记录,如果没有则返回null + Task GetPendingRecordAsync(long userId, long typeId); } diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs index db78753..8e6353b 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs @@ -614,4 +614,43 @@ public class AssessmentService : IAssessmentService return options; } + + /// + public async Task GetPendingRecordAsync(long userId, long typeId) + { + _logger.LogDebug("查询进行中的测评记录,userId: {UserId}, typeId: {TypeId}", userId, typeId); + + var record = await _dbContext.AssessmentRecords + .AsNoTracking() + .Where(r => r.UserId == userId + && r.AssessmentTypeId == typeId + && (r.Status == 1 || r.Status == 2) + && !r.IsDeleted) + .OrderByDescending(r => r.CreateTime) + .Select(r => new PendingRecordDto + { + RecordId = r.Id, + OrderId = r.OrderId, + AssessmentTypeId = r.AssessmentTypeId, + Name = r.Name, + Phone = r.Phone, + Gender = r.Gender, + Age = r.Age, + EducationStage = r.EducationStage, + Province = r.Province, + City = r.City, + District = r.District, + Status = r.Status, + CreateTime = r.CreateTime + }) + .FirstOrDefaultAsync(); + + if (record != null) + { + _logger.LogInformation("找到进行中的测评记录,userId: {UserId}, recordId: {RecordId}, status: {Status}", + userId, record.RecordId, record.Status); + } + + return record; + } } diff --git a/server/MiAssessment/src/MiAssessment.Model/Models/Assessment/PendingRecordDto.cs b/server/MiAssessment/src/MiAssessment.Model/Models/Assessment/PendingRecordDto.cs new file mode 100644 index 0000000..f8828cc --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Model/Models/Assessment/PendingRecordDto.cs @@ -0,0 +1,72 @@ +namespace MiAssessment.Model.Models.Assessment; + +/// +/// 进行中的测评记录DTO +/// +public class PendingRecordDto +{ + /// + /// 测评记录ID + /// + public long RecordId { get; set; } + + /// + /// 订单ID + /// + public long OrderId { get; set; } + + /// + /// 测评类型ID + /// + public long AssessmentTypeId { get; set; } + + /// + /// 测评人姓名 + /// + public string Name { get; set; } = null!; + + /// + /// 手机号 + /// + public string Phone { get; set; } = null!; + + /// + /// 性别:1男 2女 + /// + public int Gender { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + + /// + /// 学业阶段 + /// + public int EducationStage { get; set; } + + /// + /// 省份 + /// + public string Province { get; set; } = null!; + + /// + /// 城市 + /// + public string City { get; set; } = null!; + + /// + /// 区县 + /// + public string District { get; set; } = null!; + + /// + /// 记录状态:1待测评 2测评中 + /// + public int Status { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreateTime { get; set; } +} diff --git a/uniapp/api/assessment.js b/uniapp/api/assessment.js index b333b5a..17f2af8 100644 --- a/uniapp/api/assessment.js +++ b/uniapp/api/assessment.js @@ -25,8 +25,7 @@ export function getQuestionList(typeId) { /** * 提交答案 * @param {Object} data - 提交数据 - * @param {number} data.typeId - 测评类型ID - * @param {Object} data.userInfo - 用户信息 + * @param {number} data.recordId - 测评记录ID * @param {Array} data.answers - 答案列表 * @returns {Promise} */ @@ -81,6 +80,15 @@ export function getScoreOptions(typeId) { return get('/assessment/getScoreOptions', { typeId }) } +/** + * 获取用户进行中的测评记录 + * @param {number} typeId - 测评类型ID + * @returns {Promise} + */ +export function getPendingRecord(typeId) { + return get('/assessment/getPendingRecord', { typeId }) +} + export default { getIntro, getQuestionList, @@ -89,5 +97,6 @@ export default { getResult, verifyInviteCode, getHistoryList, - getScoreOptions + getScoreOptions, + getPendingRecord } diff --git a/uniapp/composables/usePayment.js b/uniapp/composables/usePayment.js index a8d1848..fb3244d 100644 --- a/uniapp/composables/usePayment.js +++ b/uniapp/composables/usePayment.js @@ -141,7 +141,7 @@ export function usePayment() { // 3. 查询支付结果 const result = await checkPayResult(order.orderId) if (result && result.paid) { - return { success: true, orderId: order.orderId } + return { success: true, orderId: order.orderId, assessmentRecordId: order.assessmentRecordId } } return { success: false, orderId: order.orderId, error: '支付结果确认失败' } diff --git a/uniapp/pages/assessment/info/index.vue b/uniapp/pages/assessment/info/index.vue index a7c4989..f35b9f9 100644 --- a/uniapp/pages/assessment/info/index.vue +++ b/uniapp/pages/assessment/info/index.vue @@ -7,6 +7,7 @@ * - 表单填写:姓名、手机号、性别、年龄、学业阶段、省市区 * - 支付测评和邀请码免费测评两个入口 * - 邀请码验证弹窗 + * - 进行中测评记录恢复(断点续答) */ import { ref, computed, watch } from 'vue' @@ -14,7 +15,8 @@ import { onLoad, onUnload } from '@dcloudio/uni-app' import { useUserStore } from '@/store/user.js' import { useAuth } from '@/composables/useAuth.js' import { usePayment } from '@/composables/usePayment.js' -import { getIntro, verifyInviteCode } from '@/api/assessment.js' +import { getIntro, verifyInviteCode, getPendingRecord } from '@/api/assessment.js' +import { createOrder } from '@/api/order.js' import { isPhone } from '@/utils/validate.js' import Navbar from '@/components/Navbar/index.vue' @@ -62,6 +64,21 @@ function getEducationStageValue(label) { return map[label] || 1 } +/** + * 数值转学业阶段文本 + */ +function getEducationStageLabel(value) { + const map = { 1: '小学及以下', 2: '初中', 3: '高中', 4: '大专' } + return map[value] || '' +} + +/** + * 数值转性别文本 + */ +function getGenderLabel(value) { + return value === 1 ? '男' : value === 2 ? '女' : '' +} + // 选择器索引 const genderIndex = ref(-1) const ageIndex = ref(-1) @@ -78,6 +95,10 @@ const inviteLoading = ref(false) // 页面加载状态 const pageLoading = ref(true) +// 进行中的测评记录 +const pendingRecord = ref(null) +const showPendingPopup = ref(false) + // 本地缓存 key const STORAGE_KEY = 'assessment_info_form' @@ -156,6 +177,19 @@ const isFormComplete = computed(() => { ) }) +/** + * 是否处于继续上次测评模式 + */ +const isContinueMode = computed(() => pendingRecord.value !== null) + +/** + * 底部按钮文案 + */ +const payBtnText = computed(() => { + if (isContinueMode.value) return '开始答题' + return `支付¥${introData.value.price || 20}元开始测评` +}) + /** * 加载测评介绍 */ @@ -178,6 +212,56 @@ async function loadIntro() { } } +/** + * 检查是否有进行中的测评记录 + */ +async function checkPendingRecord() { + if (!userStore.isLoggedIn) return + + try { + const res = await getPendingRecord(typeId.value) + if (res && res.code === 0 && res.data) { + pendingRecord.value = res.data + showPendingPopup.value = true + } + } catch (error) { + console.error('查询进行中测评记录失败:', error) + } +} + +/** + * 用户选择继续上次测评 + */ +function handleContinuePending() { + showPendingPopup.value = false + const record = pendingRecord.value + + // 回填个人信息到表单 + formData.value.name = record.name || '' + formData.value.phone = record.phone || '' + formData.value.gender = getGenderLabel(record.gender) + formData.value.age = record.age ? `${record.age}岁` : '' + formData.value.educationStage = getEducationStageLabel(record.educationStage) + formData.value.province = record.province || '' + formData.value.city = record.city || '' + formData.value.district = record.district || '' + + // 同步选择器索引 + genderIndex.value = record.gender === 1 ? 0 : record.gender === 2 ? 1 : -1 + ageIndex.value = record.age ? record.age - 10 : -1 + const eduIdx = educationOptions.indexOf(getEducationStageLabel(record.educationStage)) + educationIndex.value = eduIdx >= 0 ? eduIdx : -1 + regionValue.value = [record.province || '', record.city || '', record.district || ''] +} + +/** + * 用户选择不继续上次测评 + */ +function handleDismissPending() { + showPendingPopup.value = false + pendingRecord.value = null +} + /** 性别选择 */ function onGenderChange(e) { genderIndex.value = e.detail.value @@ -234,9 +318,35 @@ function validateForm() { return true } -/** 支付测评 */ +/** + * 构建测评信息对象 + */ +function buildAssessmentInfo() { + return { + name: formData.value.name, + phone: formData.value.phone, + gender: formData.value.gender === '男' ? 1 : 2, + age: parseInt(formData.value.age) || 0, + educationStage: getEducationStageValue(formData.value.educationStage), + province: formData.value.province, + city: formData.value.city, + district: formData.value.district + } +} + +/** 支付测评 / 继续上次测评 */ async function handlePayAssessment() { if (!checkLogin()) return + + // 继续上次测评模式:直接跳转答题页 + if (isContinueMode.value) { + clearFormStorage() + uni.redirectTo({ + url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${pendingRecord.value.recordId}` + }) + return + } + if (!validateForm()) return try { @@ -244,23 +354,16 @@ async function handlePayAssessment() { const result = await processPayment({ orderType: 1, productId: typeId.value, - assessmentInfo: { - name: formData.value.name, - phone: formData.value.phone, - gender: formData.value.gender === '男' ? 1 : 2, - age: parseInt(formData.value.age) || 0, - educationStage: getEducationStageValue(formData.value.educationStage), - province: formData.value.province, - city: formData.value.city, - district: formData.value.district - } + assessmentInfo: buildAssessmentInfo() }) uni.hideLoading() if (result.success) { clearFormStorage() + // 从 createOrder 返回中获取 assessmentRecordId + const recordId = result.assessmentRecordId || '' uni.redirectTo({ - url: `/pages/assessment/questions/index?typeId=${typeId.value}&orderId=${result.orderId}` + url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${recordId}` }) } else if (result.error) { uni.showToast({ title: result.error, icon: 'none' }) @@ -300,22 +403,42 @@ async function submitInviteCode() { inviteLoading.value = true try { + // 1. 验证邀请码 const res = await verifyInviteCode(inviteCode.value.trim()) - if (res && res.code === 0 && res.data && res.data.isValid) { - closeInvitePopup() - clearFormStorage() - uni.redirectTo({ - url: `/pages/assessment/questions/index?typeId=${typeId.value}&inviteCode=${inviteCode.value}` - }) - } else { + if (!res || res.code !== 0 || !res.data || !res.data.isValid) { uni.showToast({ title: res?.data?.errorMessage || res?.message || '邀请码有误,请重新输入', icon: 'none' }) + return + } + + // 2. 邀请码有效,创建免费订单(带 inviteCodeId) + const inviteCodeId = res.data.inviteCodeId + uni.showLoading({ title: '创建订单中...' }) + + const orderRes = await createOrder({ + orderType: 1, + productId: typeId.value, + assessmentInfo: buildAssessmentInfo(), + inviteCodeId: inviteCodeId + }) + uni.hideLoading() + + if (orderRes && orderRes.code === 0 && orderRes.data) { + closeInvitePopup() + clearFormStorage() + const recordId = orderRes.data.assessmentRecordId || '' + uni.redirectTo({ + url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${recordId}` + }) + } else { + uni.showToast({ title: orderRes?.message || '创建订单失败', icon: 'none' }) } } catch (error) { - console.error('验证邀请码失败:', error) - uni.showToast({ title: '验证失败,请重试', icon: 'none' }) + uni.hideLoading() + console.error('邀请码提交失败:', error) + uni.showToast({ title: '提交失败,请重试', icon: 'none' }) } finally { inviteLoading.value = false } @@ -336,6 +459,8 @@ onLoad((options) => { } loadIntro() + // 检查是否有进行中的测评 + checkPendingRecord() }) /** 页面卸载时保存 */ @@ -364,7 +489,9 @@ onUnload(() => { - 正式测评前,请先填写您的基本信息 + + {{ isContinueMode ? '您有一次未完成的测评,信息已自动填充' : '正式测评前,请先填写您的基本信息' }} + @@ -376,6 +503,7 @@ onUnload(() => { placeholder-class="input-placeholder" v-model="formData.name" maxlength="20" + :disabled="isContinueMode" /> @@ -390,14 +518,15 @@ onUnload(() => { placeholder-class="input-placeholder" v-model="formData.phone" maxlength="11" + :disabled="isContinueMode" /> - *姓别 - + *性别 + {{ formData.gender || '请选择' }} @@ -410,7 +539,7 @@ onUnload(() => { *年龄 - + {{ formData.age || '请选择' }} @@ -423,7 +552,7 @@ onUnload(() => { *学业阶段 - + {{ formData.educationStage || '请选择' }} @@ -436,7 +565,7 @@ onUnload(() => { *所在城市 - + @@ -463,11 +592,38 @@ onUnload(() => { - + 邀请码免费测评 - - 支付¥{{ introData.price || 20 }}元开始测评 + + {{ payBtnText }} + + + + + + + + 提示 + + × + + + + 您有一次未完成的测评,是否继续上次的测评? + + 姓名:{{ pendingRecord?.name }} + 手机号:{{ pendingRecord?.phone }} + + + + + 重新开始 + + + 继续测评 + + @@ -615,7 +771,6 @@ onUnload(() => { font-size: $font-size-md; color: $text-placeholder; } - } // CSS 下拉箭头 @@ -711,9 +866,13 @@ onUnload(() => { &:active { opacity: 0.8; } + + &.btn-full { + flex: 1; + } } -// ========== 邀请码弹窗 ========== +// ========== 弹窗通用 ========== .popup-mask { position: fixed; top: 0; @@ -768,18 +927,6 @@ onUnload(() => { padding: $spacing-xl $spacing-lg; } -.invite-input { - width: 100%; - height: 88rpx; - background-color: $bg-gray; - border-radius: $border-radius-md; - padding: 0 $spacing-lg; - font-size: $font-size-lg; - color: $text-color; - text-align: center; - letter-spacing: 8rpx; -} - .popup-footer { padding: 0 $spacing-lg $spacing-xl; } @@ -807,4 +954,69 @@ onUnload(() => { pointer-events: none; } } + +.popup-btn-outline { + height: 88rpx; + background-color: $bg-white; + border: 2rpx solid $border-color; + border-radius: 44rpx; + display: flex; + align-items: center; + justify-content: center; + + text { + font-size: $font-size-lg; + font-weight: $font-weight-medium; + color: $text-secondary; + } + + &:active { + opacity: 0.8; + } +} + +// ========== 进行中测评弹窗 ========== +.pending-msg { + font-size: $font-size-md; + color: $text-color; + text-align: center; + margin-bottom: $spacing-lg; +} + +.pending-info { + display: flex; + flex-direction: column; + gap: $spacing-sm; + padding: $spacing-md $spacing-lg; + background-color: $bg-gray; + border-radius: $border-radius-md; + + text { + font-size: $font-size-sm; + color: $text-secondary; + } +} + +.pending-footer { + display: flex; + gap: $spacing-md; + + .popup-btn-outline, + .popup-btn { + flex: 1; + } +} + +// ========== 邀请码输入 ========== +.invite-input { + width: 100%; + height: 88rpx; + background-color: $bg-gray; + border-radius: $border-radius-md; + padding: 0 $spacing-lg; + font-size: $font-size-lg; + color: $text-color; + text-align: center; + letter-spacing: 8rpx; +} diff --git a/uniapp/pages/assessment/questions/index.vue b/uniapp/pages/assessment/questions/index.vue index 9c96ce1..730ee94 100644 --- a/uniapp/pages/assessment/questions/index.vue +++ b/uniapp/pages/assessment/questions/index.vue @@ -19,8 +19,7 @@ const userStore = useUserStore() // 页面参数 const typeId = ref(0) -const orderId = ref('') -const inviteCode = ref('') +const recordId = ref(0) // 题目数据 const questions = ref([]) @@ -141,19 +140,17 @@ async function handleSubmit() { const answerList = questions.value.map((q, index) => { const qId = q.id || index + 1 const scoreIndex = answers.value[qId] - return { questionId: qId, score: scoreOptions.value[scoreIndex].score } + return { questionId: qId, answerValue: scoreOptions.value[scoreIndex].score } }) const res = await submitAnswers({ - typeId: typeId.value, - orderId: orderId.value, - inviteCode: inviteCode.value, + recordId: recordId.value, answers: answerList }) if (res && res.code === 0) { - const recordId = res.data?.recordId || res.data?.id || '' - uni.redirectTo({ url: `/pages/assessment/loading/index?recordId=${recordId}` }) + const resRecordId = res.data?.recordId || res.data?.id || recordId.value + uni.redirectTo({ url: `/pages/assessment/loading/index?recordId=${resRecordId}` }) } else { uni.showToast({ title: res?.message || '提交失败,请重试', icon: 'none' }) } @@ -183,8 +180,7 @@ function scrollToQuestion(questionNo) { /** 页面加载 */ onLoad((options) => { typeId.value = Number(options.typeId) || 1 - orderId.value = options.orderId || '' - inviteCode.value = options.inviteCode || '' + recordId.value = Number(options.recordId) || 0 userStore.restoreFromStorage() loadQuestions() })