diff --git a/server/MiAssessment/scripts/add_planner_booking_fields.sql b/server/MiAssessment/scripts/add_planner_booking_fields.sql new file mode 100644 index 0000000..602dc14 --- /dev/null +++ b/server/MiAssessment/scripts/add_planner_booking_fields.sql @@ -0,0 +1,46 @@ +-- ============================================= +-- 规划预约表新增字段:所在地区、学校、学情 +-- ============================================= + +-- 省份 +IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[planner_bookings]') AND name = 'Province') +BEGIN + ALTER TABLE [dbo].[planner_bookings] ADD [Province] NVARCHAR(50) NULL; + PRINT N'Column Province added to planner_bookings'; +END +GO + +-- 城市 +IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[planner_bookings]') AND name = 'City') +BEGIN + ALTER TABLE [dbo].[planner_bookings] ADD [City] NVARCHAR(50) NULL; + PRINT N'Column City added to planner_bookings'; +END +GO + +-- 区县 +IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[planner_bookings]') AND name = 'District') +BEGIN + ALTER TABLE [dbo].[planner_bookings] ADD [District] NVARCHAR(50) NULL; + PRINT N'Column District added to planner_bookings'; +END +GO + +-- 学校 +IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[planner_bookings]') AND name = 'School') +BEGIN + ALTER TABLE [dbo].[planner_bookings] ADD [School] NVARCHAR(100) NULL; + PRINT N'Column School added to planner_bookings'; +END +GO + +-- 学情 +IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[planner_bookings]') AND name = 'StudyInfo') +BEGIN + ALTER TABLE [dbo].[planner_bookings] ADD [StudyInfo] NVARCHAR(1000) NULL; + PRINT N'Column StudyInfo added to planner_bookings'; +END +GO + +PRINT N'planner_bookings 新字段添加完成'; +GO diff --git a/server/MiAssessment/scripts/init_business_db.sql b/server/MiAssessment/scripts/init_business_db.sql index 9e60231..7f03888 100644 --- a/server/MiAssessment/scripts/init_business_db.sql +++ b/server/MiAssessment/scripts/init_business_db.sql @@ -472,6 +472,13 @@ BEGIN [ScoreBiology] INT NULL, -- 生物成绩 [ScoreGeography] INT NULL, -- 地理成绩 [ScorePolitics] INT NULL, -- 政治成绩 + [FamilyAtmosphere] NVARCHAR(500) NULL, -- 家庭氛围 + [Expectation] NVARCHAR(500) NULL, -- 期望 + [Province] NVARCHAR(50) NULL, -- 省份 + [City] NVARCHAR(50) NULL, -- 城市 + [District] NVARCHAR(50) NULL, -- 区县 + [School] NVARCHAR(100) NULL, -- 学校 + [StudyInfo] NVARCHAR(1000) NULL, -- 学情 [Status] INT NOT NULL DEFAULT 1, -- 状态:1待联系 2已联系 3已完成 4已取消 [CreateTime] DATETIME2 NOT NULL DEFAULT GETDATE(), [UpdateTime] DATETIME2 NOT NULL DEFAULT GETDATE(), diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/PlannerBooking.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/PlannerBooking.cs index 1a78f56..58c5f7a 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/PlannerBooking.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Entities/PlannerBooking.cs @@ -126,6 +126,36 @@ public class PlannerBooking [MaxLength(500)] public string? Expectation { get; set; } + /// + /// 省份 + /// + [MaxLength(50)] + public string? Province { get; set; } + + /// + /// 城市 + /// + [MaxLength(50)] + public string? City { get; set; } + + /// + /// 区县 + /// + [MaxLength(50)] + public string? District { get; set; } + + /// + /// 学校 + /// + [MaxLength(100)] + public string? School { get; set; } + + /// + /// 学情 + /// + [MaxLength(1000)] + public string? StudyInfo { get; set; } + /// /// 状态:1待联系 2联系中 3已完成 4已取消 /// 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 f5da631..74e7fe5 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordDto.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordDto.cs @@ -15,6 +15,11 @@ public class AssessmentRecordDto /// public long UserId { get; set; } + /// + /// 用户UID + /// + public string? UserUid { get; set; } + /// /// 用户昵称 /// diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordQueryRequest.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordQueryRequest.cs index 425a2ea..a9fab96 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordQueryRequest.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/AssessmentRecord/AssessmentRecordQueryRequest.cs @@ -6,9 +6,9 @@ namespace MiAssessment.Admin.Business.Models.AssessmentRecord; public class AssessmentRecordQueryRequest : PagedRequest { /// - /// 用户ID + /// 用户UID /// - public long? UserId { get; set; } + public string? Uid { get; set; } /// /// 测评类型ID diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Planner/BookingDetailDto.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Planner/BookingDetailDto.cs index 6ba2c76..1877418 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Planner/BookingDetailDto.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Models/Planner/BookingDetailDto.cs @@ -94,4 +94,29 @@ public class BookingDetailDto : BookingDto /// 期望 /// public string? Expectation { get; set; } + + /// + /// 省份 + /// + public string? Province { get; set; } + + /// + /// 城市 + /// + public string? City { get; set; } + + /// + /// 区县 + /// + public string? District { get; set; } + + /// + /// 学校 + /// + public string? School { get; set; } + + /// + /// 学情 + /// + public string? StudyInfo { 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 f7240ff..a4ceae0 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/AssessmentRecordService.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/AssessmentRecordService.cs @@ -100,6 +100,7 @@ public class AssessmentRecordService : IAssessmentRecordService { Id = r.Id, UserId = r.UserId, + UserUid = r.User != null ? r.User.Uid : null, UserNickname = r.User != null ? r.User.Nickname : null, OrderId = r.OrderId, OrderNo = r.Order != null ? r.Order.OrderNo : null, @@ -693,10 +694,10 @@ public class AssessmentRecordService : IAssessmentRecordService IQueryable query, AssessmentRecordQueryRequest request) { - // 按用户ID筛选 - if (request.UserId.HasValue) + // 按用户UID筛选 + if (!string.IsNullOrWhiteSpace(request.Uid)) { - query = query.Where(r => r.UserId == request.UserId.Value); + query = query.Where(r => r.User != null && r.User.Uid == request.Uid); } // 按测评类型ID筛选 diff --git a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/PlannerService.cs b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/PlannerService.cs index 7cb3f39..5b260c6 100644 --- a/server/MiAssessment/src/MiAssessment.Admin.Business/Services/PlannerService.cs +++ b/server/MiAssessment/src/MiAssessment.Admin.Business/Services/PlannerService.cs @@ -402,6 +402,11 @@ public class PlannerService : IPlannerService ScorePolitics = booking.ScorePolitics, FamilyAtmosphere = booking.FamilyAtmosphere, Expectation = booking.Expectation, + Province = booking.Province, + City = booking.City, + District = booking.District, + School = booking.School, + StudyInfo = booking.StudyInfo, Status = booking.Status, StatusName = GetBookingStatusName(booking.Status), OrderAmount = booking.Order?.Amount, 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 f19fabd..3ddc44b 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 @@ -12,6 +12,7 @@ import type { PagedRequest, PagedResult } from '@/types/common' export interface AssessmentRecordItem { id: number userId: number + userUid: string | null userNickname: string | null orderId: number orderNo: string | null @@ -92,7 +93,7 @@ export interface AssessmentReport extends AssessmentRecordItem { /** 测评记录查询参数 */ export interface AssessmentRecordQuery extends PagedRequest { - userId?: number + uid?: string assessmentTypeId?: number status?: number startDate?: string 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 353dcfc..57a3a5e 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 @@ -23,16 +23,16 @@ - + - + @@ -40,6 +40,7 @@ + @@ -72,10 +73,10 @@ - + {{ row.userNickname || '-' }} - ID: {{ row.userId }} + UID: {{ row.userUid || '-' }} @@ -452,7 +453,7 @@ const dateRange = ref<[string, string] | null>(null) const queryParams = reactive({ page: 1, pageSize: 10, - userId: '', + uid: '', status: undefined as number | undefined, startDate: undefined as string | undefined, endDate: undefined as string | undefined @@ -551,8 +552,8 @@ async function loadRecordList() { page: queryParams.page, pageSize: queryParams.pageSize } - if (queryParams.userId) { - params.userId = Number(queryParams.userId) + if (queryParams.uid) { + params.uid = queryParams.uid } if (queryParams.status !== undefined) { params.status = queryParams.status @@ -641,7 +642,7 @@ function handleSearch() { } function handleReset() { - queryParams.userId = '' + queryParams.uid = '' queryParams.status = undefined queryParams.startDate = undefined queryParams.endDate = undefined @@ -816,8 +817,8 @@ async function handleExport() { page: 1, pageSize: 10000 } - if (queryParams.userId) { - params.userId = Number(queryParams.userId) + if (queryParams.uid) { + params.uid = queryParams.uid } if (queryParams.status !== undefined) { params.status = queryParams.status diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/config/index.vue b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/config/index.vue index 29cd64f..f0c6d4a 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/config/index.vue +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/config/index.vue @@ -44,9 +44,9 @@ 点击编辑上传图片 @@ -166,6 +166,17 @@ + + + + + + @@ -201,6 +212,8 @@ interface ConfigPageState { imageValue: string saving: boolean validationError: string + previewVisible: boolean + previewUrl: string } // 富文本配置键列表 @@ -229,7 +242,9 @@ const state = reactive({ imageDialogVisible: false, imageValue: '', saving: false, - validationError: '' + validationError: '', + previewVisible: false, + previewUrl: '' }) // ============ Helper Functions ============ @@ -464,6 +479,14 @@ function handleImageCancel() { state.imageValue = '' } +/** + * 预览图片 + */ +function handlePreviewImage(url: string) { + state.previewUrl = url + state.previewVisible = true +} + async function handleImageSave() { if (!state.editingItem) return diff --git a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/planner/booking/index.vue b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/planner/booking/index.vue index 574a754..8cddeb8 100644 --- a/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/planner/booking/index.vue +++ b/server/MiAssessment/src/MiAssessment.Admin/admin-web/src/views/business/planner/booking/index.vue @@ -115,6 +115,9 @@ {{ detailData.name }} {{ detailData.gradeName }} {{ detailData.phone }} + {{ [detailData.province, detailData.city, detailData.district].filter(Boolean).join(' ') || '-' }} + {{ detailData.school || '-' }} + {{ detailData.studyInfo || '-' }} {{ detailData.majorName || '-' }} {{ detailData.scoreChinese ?? '-' }} {{ detailData.scoreMath ?? '-' }} diff --git a/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs b/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs index a0fa63c..b251a96 100644 --- a/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs +++ b/server/MiAssessment/src/MiAssessment.Api/Controllers/AssessmentController.cs @@ -413,7 +413,7 @@ public class AssessmentController : ControllerBase /// /// GET /api/assessment/getPendingRecord?typeId=1 /// - /// 查询当前用户状态为待测评或测评中的最新测评记录,用于断点续答。 + /// 查询当前用户状态为待测评、测评中、生成失败或需重测的最新测评记录,用于断点续答和失败重测。 /// 需要用户登录认证。 /// /// 测评类型ID @@ -446,4 +446,55 @@ public class AssessmentController : ControllerBase return ApiResponse.Fail("查询进行中的测评记录失败"); } } + + /// + /// 重新测评(生成失败或需重测时免费重测) + /// + /// + /// POST /api/assessment/retest + /// + /// 将状态为生成失败(5)或需重测(7)的测评记录重置为待测评(1), + /// 清除旧的答案、结果和结论数据,允许用户免费重新答题。 + /// 需要用户登录认证。 + /// + /// 重测请求 + /// 重测结果 + [HttpPost("retest")] + [Authorize] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] + public async Task> Retest([FromBody] RetestRequest request) + { + var userId = GetCurrentUserId(); + if (userId == null) + { + return ApiResponse.Unauthorized(); + } + + try + { + if (request == null || request.RecordId <= 0) + { + return ApiResponse.Fail("测评记录ID无效"); + } + + var result = await _assessmentService.RetestAsync(userId.Value, request.RecordId); + return ApiResponse.Success(result); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogWarning(ex, "Unauthorized retest, userId: {UserId}, recordId: {RecordId}", userId, request?.RecordId); + return ApiResponse.Fail("无权限访问该测评记录", -1); + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, "Invalid retest, userId: {UserId}, recordId: {RecordId}", userId, request?.RecordId); + return ApiResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retest, userId: {UserId}, recordId: {RecordId}", userId, request?.RecordId); + return ApiResponse.Fail("重新测评失败"); + } + } } diff --git a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs index f639b64..0843428 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IAssessmentService.cs @@ -116,11 +116,23 @@ public interface IAssessmentService /// 获取用户进行中的测评记录 /// /// - /// 查询当前用户状态为待测评(1)或测评中(2)的最新测评记录。 - /// 用于页面加载时检测是否有未完成的测评,支持断点续答。 + /// 查询当前用户状态为待测评(1)、测评中(2)、生成失败(5)或需重测(7)的最新测评记录。 + /// 用于页面加载时检测是否有未完成的测评,支持断点续答和失败重测。 /// /// 当前用户ID /// 测评类型ID /// 进行中的测评记录,如果没有则返回null Task GetPendingRecordAsync(long userId, long typeId); + + /// + /// 重新测评(生成失败或需重测时免费重测) + /// + /// + /// 将状态为生成失败(5)或需重测(7)的测评记录重置为待测评(1), + /// 清除旧的答案、结果和结论数据,允许用户免费重新答题。 + /// + /// 当前用户ID + /// 测评记录ID + /// 重置是否成功 + Task RetestAsync(long userId, long recordId); } diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs index 5d90d33..bd585c1 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/AssessmentService.cs @@ -695,7 +695,7 @@ public class AssessmentService : IAssessmentService .AsNoTracking() .Where(r => r.UserId == userId && r.AssessmentTypeId == typeId - && (r.Status == 1 || r.Status == 2 || r.Status == 7) + && (r.Status == 1 || r.Status == 2 || r.Status == 5 || r.Status == 7) && !r.IsDeleted) .OrderByDescending(r => r.CreateTime) .Select(r => new PendingRecordDto @@ -724,4 +724,81 @@ public class AssessmentService : IAssessmentService return record; } + + /// + public async Task RetestAsync(long userId, long recordId) + { + _logger.LogInformation("重新测评请求,userId: {UserId}, recordId: {RecordId}", userId, recordId); + + var record = await _dbContext.AssessmentRecords + .Where(r => r.Id == recordId && !r.IsDeleted) + .FirstOrDefaultAsync(); + + if (record == null) + { + _logger.LogWarning("测评记录不存在,recordId: {RecordId}", recordId); + throw new InvalidOperationException("测评记录不存在"); + } + + if (record.UserId != userId) + { + _logger.LogWarning("测评记录不属于当前用户,userId: {UserId}, recordUserId: {RecordUserId}", userId, record.UserId); + throw new UnauthorizedAccessException("无权限访问该测评记录"); + } + + // 只有生成失败(5)和需重测(7)状态才允许重测 + if (record.Status != 5 && record.Status != 7) + { + _logger.LogWarning("测评记录状态不允许重测,status: {Status}, recordId: {RecordId}", record.Status, recordId); + throw new InvalidOperationException("当前测评状态不允许重新测评"); + } + + using var transaction = await _dbContext.Database.BeginTransactionAsync(); + try + { + // 清除旧答案 + var oldAnswers = await _dbContext.AssessmentAnswers + .Where(a => a.RecordId == recordId) + .ToListAsync(); + if (oldAnswers.Any()) + { + _dbContext.AssessmentAnswers.RemoveRange(oldAnswers); + } + + // 清除旧结果 + var oldResults = await _dbContext.AssessmentResults + .Where(r => r.RecordId == recordId) + .ToListAsync(); + if (oldResults.Any()) + { + _dbContext.AssessmentResults.RemoveRange(oldResults); + } + + // 清除旧结论 + var oldConclusions = await _dbContext.AssessmentRecordConclusions + .Where(c => c.RecordId == recordId) + .ToListAsync(); + if (oldConclusions.Any()) + { + _dbContext.AssessmentRecordConclusions.RemoveRange(oldConclusions); + } + + // 重置状态为待测评 + record.Status = 1; + record.SubmitTime = null; + record.UpdateTime = DateTime.Now; + + await _dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); + + _logger.LogInformation("重新测评重置成功,recordId: {RecordId}, userId: {UserId}", recordId, userId); + return true; + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + _logger.LogError(ex, "重新测评重置失败,recordId: {RecordId}", recordId); + throw; + } + } } diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/OrderService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/OrderService.cs index 42a1dff..2647e51 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/OrderService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/OrderService.cs @@ -681,6 +681,11 @@ public class OrderService : IOrderService ScorePolitics = request.PlannerInfo.ScorePolitics, FamilyAtmosphere = request.PlannerInfo.FamilyAtmosphere, Expectation = request.PlannerInfo.Expectation, + Province = request.PlannerInfo.Province, + City = request.PlannerInfo.City, + District = request.PlannerInfo.District, + School = request.PlannerInfo.School, + StudyInfo = request.PlannerInfo.StudyInfo, Status = 1, // 待确认 CreateTime = now, UpdateTime = now, diff --git a/server/MiAssessment/src/MiAssessment.Model/Entities/PlannerBooking.cs b/server/MiAssessment/src/MiAssessment.Model/Entities/PlannerBooking.cs index 3ce113e..486321f 100644 --- a/server/MiAssessment/src/MiAssessment.Model/Entities/PlannerBooking.cs +++ b/server/MiAssessment/src/MiAssessment.Model/Entities/PlannerBooking.cs @@ -126,6 +126,36 @@ public class PlannerBooking [MaxLength(500)] public string? Expectation { get; set; } + /// + /// 省份 + /// + [MaxLength(50)] + public string? Province { get; set; } + + /// + /// 城市 + /// + [MaxLength(50)] + public string? City { get; set; } + + /// + /// 区县 + /// + [MaxLength(50)] + public string? District { get; set; } + + /// + /// 学校 + /// + [MaxLength(100)] + public string? School { get; set; } + + /// + /// 学情 + /// + [MaxLength(1000)] + public string? StudyInfo { get; set; } + /// /// 状态:1待联系 2联系中 3已完成 4已取消 /// diff --git a/server/MiAssessment/src/MiAssessment.Model/Models/Assessment/RetestRequest.cs b/server/MiAssessment/src/MiAssessment.Model/Models/Assessment/RetestRequest.cs new file mode 100644 index 0000000..4f013ec --- /dev/null +++ b/server/MiAssessment/src/MiAssessment.Model/Models/Assessment/RetestRequest.cs @@ -0,0 +1,12 @@ +namespace MiAssessment.Model.Models.Assessment; + +/// +/// 重新测评请求 +/// +public class RetestRequest +{ + /// + /// 测评记录ID + /// + public long RecordId { get; set; } +} diff --git a/server/MiAssessment/src/MiAssessment.Model/Models/Order/CreateOrderModels.cs b/server/MiAssessment/src/MiAssessment.Model/Models/Order/CreateOrderModels.cs index 65e5c28..d44ace0 100644 --- a/server/MiAssessment/src/MiAssessment.Model/Models/Order/CreateOrderModels.cs +++ b/server/MiAssessment/src/MiAssessment.Model/Models/Order/CreateOrderModels.cs @@ -157,6 +157,31 @@ public class PlannerInfoDto /// public string? Expectation { get; set; } + /// + /// 省份 + /// + public string? Province { get; set; } + + /// + /// 城市 + /// + public string? City { get; set; } + + /// + /// 区县 + /// + public string? District { get; set; } + + /// + /// 学校 + /// + public string? School { get; set; } + + /// + /// 学情 + /// + public string? StudyInfo { get; set; } + /// /// 备注 /// diff --git a/uniapp/api/assessment.js b/uniapp/api/assessment.js index 17f2af8..b31e757 100644 --- a/uniapp/api/assessment.js +++ b/uniapp/api/assessment.js @@ -89,6 +89,15 @@ export function getPendingRecord(typeId) { return get('/assessment/getPendingRecord', { typeId }) } +/** + * 重新测评(生成失败或需重测时免费重测) + * @param {number} recordId - 测评记录ID + * @returns {Promise} + */ +export function retestRecord(recordId) { + return post('/assessment/retest', { recordId }) +} + export default { getIntro, getQuestionList, @@ -98,5 +107,6 @@ export default { verifyInviteCode, getHistoryList, getScoreOptions, - getPendingRecord + getPendingRecord, + retestRecord } diff --git a/uniapp/pages/assessment/history/index.vue b/uniapp/pages/assessment/history/index.vue index aa77264..2aff489 100644 --- a/uniapp/pages/assessment/history/index.vue +++ b/uniapp/pages/assessment/history/index.vue @@ -7,7 +7,7 @@ import { ref, computed, onMounted } from 'vue' import { onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' import { useUserStore } from '@/store/user.js' import { useAuth } from '@/composables/useAuth.js' -import { getHistoryList } from '@/api/assessment.js' +import { getHistoryList, retestRecord } from '@/api/assessment.js' import Empty from '@/components/Empty/index.vue' import Loading from '@/components/Loading/index.vue' @@ -124,7 +124,7 @@ onReachBottom(() => { /** * 查看测评结果 */ -function viewResult(record) { +async function viewResult(record) { // 已完成 - 查看报告 if (record.status === ASSESSMENT_STATUS.COMPLETED) { uni.navigateTo({ @@ -158,23 +158,42 @@ function viewResult(record) { return } - // 需重测 - 跳转到测评首页重新开始 + // 需重测 - 调用重测接口后跳转答题页(免费重测) if (record.status === ASSESSMENT_STATUS.NEED_RETEST) { - uni.navigateTo({ - url: `/pages/assessment/info/index?typeId=${record.assessmentTypeId || 1}` - }) + await handleRetestFromHistory(record) return } - // 生成失败 - 跳转到测评首页重新开始 + // 生成失败 - 调用重测接口后跳转答题页(免费重测) if (record.status === ASSESSMENT_STATUS.FAILED) { - uni.navigateTo({ - url: `/pages/assessment/info/index?typeId=${record.assessmentTypeId || 1}` - }) + await handleRetestFromHistory(record) return } } +/** + * 从往期测评列表发起重测(免费) + */ +async function handleRetestFromHistory(record) { + try { + uni.showLoading({ title: '重置中...' }) + const res = await retestRecord(record.id) + uni.hideLoading() + + if (res && res.code === 0) { + uni.navigateTo({ + url: `/pages/assessment/questions/index?typeId=${record.assessmentTypeId || 1}&recordId=${record.id}` + }) + } else { + uni.showToast({ title: res?.message || '重置失败,请重试', icon: 'none' }) + } + } catch (error) { + uni.hideLoading() + console.error('重测重置失败:', error) + uni.showToast({ title: '重置失败,请重试', icon: 'none' }) + } +} + /** * 页面显示时检查登录状态并加载数据 */ diff --git a/uniapp/pages/assessment/info/index.vue b/uniapp/pages/assessment/info/index.vue index 6dbd7c7..1d0788a 100644 --- a/uniapp/pages/assessment/info/index.vue +++ b/uniapp/pages/assessment/info/index.vue @@ -29,7 +29,8 @@ import { getIntro, verifyInviteCode, - getPendingRecord + getPendingRecord, + retestRecord } from '@/api/assessment.js' import { createOrder @@ -203,11 +204,26 @@ ) }) + /** + * 弹窗提示文案(根据记录状态区分) + */ + const pendingPopupMessage = computed(() => { + if (!pendingRecord.value) return '' + const s = pendingRecord.value.status + if (s === 5) return '您有一份生成失败的测评记录,可免费重新测评' + if (s === 7) return '您有一份需要重新测评的记录,可免费重新测评' + return '您有一份未完成的测评记录,是否继续?' + }) + /** * 底部按钮文案 */ const payBtnText = computed(() => { - if (isContinueMode.value) return '继续测评' + if (isContinueMode.value) { + const s = pendingRecord.value?.status + if (s === 5 || s === 7) return '免费重新测评' + return '继续测评' + } return `支付¥${introData.value.price || 20}元开始测评` }) @@ -329,8 +345,28 @@ // 继续测评模式:直接跳转到答题页 if (isContinueMode.value && pendingRecord.value) { + const r = pendingRecord.value + + // 生成失败(5)或需重测(7)的记录,先调重测接口重置状态 + if (r.status === 5 || r.status === 7) { + try { + uni.showLoading({ title: '重置中...' }) + const res = await retestRecord(r.recordId) + uni.hideLoading() + if (!res || res.code !== 0) { + uni.showToast({ title: res?.message || '重置失败,请重试', icon: 'none' }) + return + } + } catch (error) { + uni.hideLoading() + console.error('重测重置失败:', error) + uni.showToast({ title: '重置失败,请重试', icon: 'none' }) + return + } + } + uni.redirectTo({ - url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${pendingRecord.value.recordId}` + url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${r.recordId}` }) return } @@ -613,7 +649,7 @@ - 您有一份未完成的测评记录,是否继续? + {{ pendingPopupMessage }} 姓名:{{ pendingRecord?.name }} diff --git a/uniapp/pages/assessment/loading/index.vue b/uniapp/pages/assessment/loading/index.vue index 8d2c005..04a06ba 100644 --- a/uniapp/pages/assessment/loading/index.vue +++ b/uniapp/pages/assessment/loading/index.vue @@ -13,7 +13,7 @@ import { ref, onMounted, onUnmounted } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { useUserStore } from '@/store/user.js' -import { getResultStatus } from '@/api/assessment.js' +import { getResultStatus, retestRecord } from '@/api/assessment.js' import Navbar from '@/components/Navbar/index.vue' const userStore = useUserStore() @@ -125,19 +125,37 @@ async function checkStatus() { } } +// 重测加载状态 +const retestLoading = ref(false) + /** - * 重新测试 + * 重新测试(调用重测接口,免费重新答题) */ -function handleRetest() { - // 跳转到测评首页重新开始 - if (typeId.value) { - uni.redirectTo({ - url: `/pages/assessment/info/index?typeId=${typeId.value}` - }) - } else { - uni.switchTab({ - url: '/pages/index/index' +async function handleRetest() { + if (retestLoading.value) return + retestLoading.value = true + + try { + const res = await retestRecord(recordId.value) + if (res && res.code === 0) { + // 重置成功,直接跳转答题页 + uni.redirectTo({ + url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${recordId.value}` + }) + } else { + uni.showToast({ + title: res?.message || '重新测评失败,请稍后重试', + icon: 'none' + }) + } + } catch (error) { + console.error('重新测评失败:', error) + uni.showToast({ + title: '重新测评失败,请稍后重试', + icon: 'none' }) + } finally { + retestLoading.value = false } } @@ -191,8 +209,8 @@ onUnmounted(() => { - - 重新测试 + + {{ retestLoading ? '重置中...' : '重新测试' }} @@ -301,5 +319,10 @@ onUnmounted(() => { &:active { opacity: 0.85; } + + &.btn-loading { + opacity: 0.7; + pointer-events: none; + } } diff --git a/uniapp/pages/index/index.vue b/uniapp/pages/index/index.vue index b54be78..d656f52 100644 --- a/uniapp/pages/index/index.vue +++ b/uniapp/pages/index/index.vue @@ -112,13 +112,24 @@ - + + + + 联系我们 + ✕ + + + + 暂未配置客服二维码 + + + @@ -130,7 +141,6 @@ import { useNavbar } from '@/composables/useNavbar.js' import { getBannerList, getNavigationList } from '@/api/home.js' import { getContactInfo } from '@/api/system.js' import Loading from '@/components/Loading/index.vue' -import Popup from '@/components/Popup/index.vue' const userStore = useUserStore() const { statusBarHeight, navbarHeight, totalNavbarHeight } = useNavbar() @@ -186,6 +196,17 @@ async function loadMoreList() { } } +/** + * 预览二维码图片 + */ +function handlePreviewQrcode() { + if (!qrcodeUrl.value) return + uni.previewImage({ + urls: [qrcodeUrl.value], + current: qrcodeUrl.value + }) +} + /** * 加载客服二维码URL */ @@ -477,4 +498,60 @@ onMounted(() => { height: 40rpx; padding-bottom: env(safe-area-inset-bottom); } + +// 联系我们弹窗 +.popup-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 999; +} + +.contact-popup { + width: 90%; + background-color: $bg-white; + border-radius: $border-radius-xl; + overflow: hidden; + + .contact-popup-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: $spacing-xl $spacing-xl 0; + + .contact-popup-title { + font-size: $font-size-lg; + font-weight: $font-weight-bold; + color: $text-color; + } + + .contact-popup-close { + font-size: $font-size-xl; + color: $text-placeholder; + } + } + + .contact-popup-body { + display: flex; + flex-direction: column; + align-items: center; + padding: $spacing-xl; + + .contact-qrcode { + width: 100%; + } + + .contact-tip { + margin-top: $spacing-lg; + font-size: $font-size-sm; + color: $text-secondary; + } + } +} diff --git a/uniapp/pages/mine/index.vue b/uniapp/pages/mine/index.vue index f2f1bd3..98d864e 100644 --- a/uniapp/pages/mine/index.vue +++ b/uniapp/pages/mine/index.vue @@ -109,10 +109,9 @@ - 长按识别或点击预览保存二维码 @@ -523,7 +522,7 @@ onMounted(() => { // 联系我们弹窗 .contact-popup { - width: 560rpx; + width: 90%; background-color: $bg-white; border-radius: $border-radius-xl; overflow: hidden; @@ -554,16 +553,9 @@ onMounted(() => { padding: $spacing-xl; .contact-qrcode { - width: 400rpx; - height: 400rpx; + width: 100%; border-radius: $border-radius-md; } - - .contact-tip { - margin-top: $spacing-lg; - font-size: $font-size-sm; - color: $text-placeholder; - } } } diff --git a/uniapp/pages/order/list/index.vue b/uniapp/pages/order/list/index.vue index f30702a..df25026 100644 --- a/uniapp/pages/order/list/index.vue +++ b/uniapp/pages/order/list/index.vue @@ -9,6 +9,7 @@ import { onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app' import { useUserStore } from '@/store/user.js' import { useAuth } from '@/composables/useAuth.js' import { getOrderList } from '@/api/order.js' +import { retestRecord } from '@/api/assessment.js' import Empty from '@/components/Empty/index.vue' import Loading from '@/components/Loading/index.vue' @@ -58,7 +59,7 @@ function getDisplayStatus(order) { // 已支付/已完成的订单,根据测评记录状态显示 if (assessmentStatus) { - const map = { 1: '待测评', 2: '测评中', 3: '测评生成中', 4: '已测评', 5: '生成失败', 7: '待测评' } + const map = { 1: '待测评', 2: '测评中', 3: '测评生成中', 4: '已测评', 5: '生成失败', 7: '需重测' } return map[assessmentStatus] || order.statusText || '已支付' } @@ -86,7 +87,7 @@ function getStatusClass(order) { const dst = getDisplayStatus(order) if (dst === '已测评' || dst === '待测评' || dst === '已完成') return 'status-green' if (dst === '测评生成中' || dst === '退款中' || dst === '已退款' || dst === '报告生成中') return 'status-red' - if (dst === '待联系' || dst === '联系中') return 'status-orange' + if (dst === '待联系' || dst === '联系中' || dst === '需重测' || dst === '生成失败') return 'status-orange' return 'status-gray' } @@ -171,13 +172,27 @@ function viewResult(order) { /** * 开始测评 */ -function startAssessment(order) { +async function startAssessment(order) { const dst = getDisplayStatus(order) - if (dst === '生成失败') { - // 生成失败:跳转到测评首页重新开始 - uni.navigateTo({ - url: `/pages/assessment/info/index?typeId=${order.productId || 1}` - }) + if (dst === '生成失败' || dst === '需重测') { + // 生成失败或需重测:调用重测接口后跳转答题页(免费重测) + try { + uni.showLoading({ title: '重置中...' }) + const res = await retestRecord(order.assessmentRecordId) + uni.hideLoading() + + if (res && res.code === 0) { + uni.navigateTo({ + url: `/pages/assessment/questions/index?typeId=${order.productId || 1}&recordId=${order.assessmentRecordId}` + }) + } else { + uni.showToast({ title: res?.message || '重置失败,请重试', icon: 'none' }) + } + } catch (error) { + uni.hideLoading() + console.error('重测重置失败:', error) + uni.showToast({ title: '重置失败,请重试', icon: 'none' }) + } } else { uni.navigateTo({ url: `/pages/assessment/questions/index?recordId=${order.assessmentRecordId}` @@ -197,7 +212,7 @@ function showViewResultBtn(order) { */ function showStartBtn(order) { const dst = getDisplayStatus(order) - return dst === '待测评' || dst === '生成失败' + return dst === '待测评' || dst === '生成失败' || dst === '需重测' } onShow(() => { diff --git a/uniapp/pages/planner/book/index.vue b/uniapp/pages/planner/book/index.vue index d89f81d..21eda17 100644 --- a/uniapp/pages/planner/book/index.vue +++ b/uniapp/pages/planner/book/index.vue @@ -48,6 +48,8 @@ const timeOptions = [ // 表单数据 const formData = ref({ name: '', phone: '', grade: '', + province: '', city: '', district: '', + school: '', studyInfo: '', majorName: '', familyAtmosphere: '', expectation: '' }) @@ -114,10 +116,12 @@ const showSuccessPopup = ref(false) * 表单是否填写完整 */ const isFormComplete = computed(() => { - if (!selectedDate.value || !selectedTime.value) return false if (!formData.value.name.trim()) return false if (!formData.value.phone.trim()) return false if (!formData.value.grade) return false + if (!formData.value.province) return false + if (!formData.value.school.trim()) return false + if (!formData.value.studyInfo.trim()) return false if (showScores.value) { if (!scores.value.chinese.trim() || !scores.value.math.trim() || !scores.value.english.trim()) return false } @@ -211,13 +215,20 @@ function onGradeChange(e) { formData.value.majorName = '' } +/** + * 省市区选择 + */ +function onRegionChange(e) { + const value = e.detail.value + formData.value.province = value[0] || '' + formData.value.city = value[1] || '' + formData.value.district = value[2] || '' +} + /** * 验证表单 */ function validateForm() { - if (!selectedDate.value || !selectedTime.value) { - uni.showToast({ title: '请选择规划时间', icon: 'none' }); return false - } if (!formData.value.name.trim()) { uni.showToast({ title: '请输入姓名', icon: 'none' }); return false } @@ -227,6 +238,15 @@ function validateForm() { if (!formData.value.grade) { uni.showToast({ title: '请选择所在年级', icon: 'none' }); return false } + if (!formData.value.province) { + uni.showToast({ title: '请选择所在地区', icon: 'none' }); return false + } + if (!formData.value.school.trim()) { + uni.showToast({ title: '请输入学校', icon: 'none' }); return false + } + if (!formData.value.studyInfo.trim()) { + uni.showToast({ title: '请输入学情', icon: 'none' }); return false + } if (showScores.value && (!scores.value.chinese.trim() || !scores.value.math.trim() || !scores.value.english.trim())) { uni.showToast({ title: '请填写必填科目成绩', icon: 'none' }); return false } @@ -278,8 +298,13 @@ async function handleSubmit() { plannerInfo: { name: formData.value.name, phone: formData.value.phone, - bookDateTime: `${selectedDate.value} ${selectedTime.value}`, + bookDateTime: selectedDate.value && selectedTime.value ? `${selectedDate.value} ${selectedTime.value}` : null, grade: gradeMap[formData.value.grade] || 0, + province: formData.value.province, + city: formData.value.city, + district: formData.value.district, + school: formData.value.school, + studyInfo: formData.value.studyInfo, majorName: showMajor.value ? formData.value.majorName : null, ...scoreFields, familyAtmosphere: formData.value.familyAtmosphere, @@ -317,9 +342,6 @@ onLoad(async (options) => { plannerId.value = Number(options.plannerId) || 0 plannerName.value = decodeURIComponent(options.plannerName || '') userStore.restoreFromStorage() - if (userStore.isLoggedIn && userStore.phone) { - formData.value.phone = userStore.phone - } await loadPlannerDetail() pageLoading.value = false }) @@ -330,22 +352,11 @@ onLoad(async (options) => { - - + + @@ -374,6 +385,36 @@ onLoad(async (options) => { + + + *所在地区 + + + {{ formData.province }} {{ formData.city }} {{ formData.district }} + 请选择 + ∨ + + + + + + + *学校 + + + + + + *学情 + + + 各科最近成绩 @@ -544,6 +585,18 @@ onLoad(async (options) => { color: $text-color; } +.field-textarea { + width: 100%; + min-height: 180rpx; + background-color: $bg-gray; + border-radius: $border-radius-sm; + padding: $spacing-lg; + font-size: $font-size-md; + color: $text-color; + box-sizing: border-box; + line-height: 1.6; +} + .field-select { display: flex; align-items: center;