diff --git a/docs/问题跟踪/后端文件上传数据丢失问题.md b/docs/问题跟踪/后端文件上传数据丢失问题.md new file mode 100644 index 00000000..2690fc35 --- /dev/null +++ b/docs/问题跟踪/后端文件上传数据丢失问题.md @@ -0,0 +1,300 @@ +# 后端文件上传数据丢失问题 + +**发现日期**: 2025-01-24 +**问题模块**: 文件上传/OSS服务 +**影响范围**: 所有使用附件上传的业务模块 +**严重程度**: ⚠️ 中等(影响用户体验,但有前端兜底) + +--- + +## 问题描述 + +后端在保存文件时丢失了真实的文件名和扩展名信息,导致前端回显时无法正确识别文件类型。 + +### 问题表现 + +从 `/resource/oss/listByIds/{ossIds}` API 返回的数据中: + +```javascript +{ + ossId: "1981738218223079425", + originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9", // ❌ UUID,不是用户上传的真实文件名 + fileSuffix: "9", // ❌ 数字字符,不是真实扩展名(应该是 jpg/png 等) + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139", // ❌ 没有扩展名 + url: "/resource/oss/download/1981738218223079425" +} +``` + +### 期望的数据格式 + +```javascript +{ + ossId: "1981738218223079425", + originalName: "违纪现场照片.jpg", // ✅ 用户上传时的真实文件名 + fileSuffix: "jpg", // ✅ 真实的文件扩展名 + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139.jpg", // ✅ 包含扩展名 + url: "/resource/oss/download/1981738218223079425" +} +``` + +--- + +## 问题分析 + +### 可能的原因 + +1. **文件上传时未保存原始文件名** + - 上传接口 `/resource/oss/upload` 可能未正确提取文件名 + - `originalName` 字段被UUID覆盖 + +2. **扩展名提取错误** + - `fileSuffix` 字段提取逻辑有误 + - 从文件路径末尾提取了一个字符(如 `9`、`e`、`d`)而不是真实扩展名 + +3. **数据库保存逻辑问题** + - `sys_oss` 表中的字段未正确保存 + - 可能是某个字段映射错误 + +### 影响的业务场景 + +- ✅ **新增上传**:正常工作(文件可以上传) +- ❌ **回显展示**:文件类型识别错误,图片无法正常显示 +- ❌ **下载功能**:下载的文件名显示为UUID,用户体验差 +- ❌ **文件管理**:管理员无法识别文件内容 + +--- + +## 复现步骤 + +1. 在移动端进入"宿舍卫生检查"模块 +2. 点击"录入违纪",上传一张图片(如 `photo.jpg`) +3. 保存违纪记录 +4. 刷新页面或重新进入 +5. 点击"编辑"查看违纪记录 +6. **问题**:附件无法显示为图片,文件名显示为UUID + +--- + +## 调试信息 + +### 前端上传请求 + +```javascript +// 调用上传接口 +POST /resource/oss/upload +Content-Type: multipart/form-data + +// FormData: +file: [Blob] photo.jpg +bizType: "dorm_violation_attachment" +``` + +### 后端返回(上传成功) + +```javascript +{ + code: 200, + data: { + ossId: "1981738218223079425", + url: "/resource/oss/download/1981738218223079425", + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139" // ⚠️ 缺少扩展名 + } +} +``` + +### 后端返回(回显查询) + +```javascript +// GET /resource/oss/listByIds/1981738218223079425 +{ + code: 200, + data: [{ + ossId: "1981738218223079425", + originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9", // ❌ UUID + fileSuffix: "9", // ❌ 错误 + fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139", + url: "/resource/oss/download/1981738218223079425" + }] +} +``` + +--- + +## 建议的修复方案 + +### 方案1:修复上传接口(推荐)⭐ + +**修改文件**:`backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java` + +**修复点**: +1. 上传时正确提取并保存 `originalName`(用户上传的文件名) +2. 正确提取文件扩展名保存到 `fileSuffix` +3. 保存到OSS的文件名应包含扩展名 + +```java +@PostMapping("/upload") +public R upload(@RequestParam("file") MultipartFile file, + @RequestParam(required = false) String bizType) { + // 获取原始文件名 + String originalFilename = file.getOriginalFilename(); + + // 提取真实扩展名 + String extension = ""; + if (originalFilename != null && originalFilename.contains(".")) { + extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + } + + // 生成OSS文件名(包含扩展名) + String ossFileName = generateOssFileName() + "." + extension; + + // 保存到数据库 + SysOss oss = new SysOss(); + oss.setOriginalName(originalFilename); // ✅ 保存真实文件名 + oss.setFileSuffix(extension); // ✅ 保存真实扩展名 + oss.setFileName(ossFileName); // ✅ 包含扩展名的OSS文件名 + + // ... 其他逻辑 +} +``` + +### 方案2:修复查询接口 + +**修改文件**:`backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java` + +**修复点**: +1. 从数据库正确读取 `originalName` 和 `fileSuffix` +2. 确保返回的VO类包含正确的字段值 + +```java +public List listByIds(String ossIds) { + // ... 查询逻辑 ... + + List voList = new ArrayList<>(); + for (SysOss oss : ossList) { + SysOssVo vo = new SysOssVo(); + vo.setOssId(oss.getOssId()); + vo.setOriginalName(oss.getOriginalName()); // ✅ 确保读取正确 + vo.setFileSuffix(oss.getFileSuffix()); // ✅ 确保读取正确 + vo.setFileName(oss.getFileName()); + vo.setUrl(oss.getUrl()); + voList.add(vo); + } + + return voList; +} +``` + +### 方案3:数据库表结构检查 + +**检查表**:`sys_oss` + +**需要确认的字段**: +```sql +-- 检查字段定义 +SHOW COLUMNS FROM sys_oss LIKE '%name%'; +SHOW COLUMNS FROM sys_oss LIKE '%suffix%'; + +-- 检查数据示例 +SELECT oss_id, original_name, file_suffix, file_name +FROM sys_oss +WHERE oss_id = '1981738218223079425'; +``` + +**期望的字段定义**: +- `original_name` VARCHAR(255) - 用户上传的原始文件名 +- `file_suffix` VARCHAR(10) - 文件扩展名(jpg/png/pdf等) +- `file_name` VARCHAR(500) - OSS中的文件路径(应包含扩展名) + +--- + +## 临时解决方案(前端兜底) + +**已实施**:在 `ViolationRecordDialog.vue` 中添加了兜底逻辑 + +```javascript +// 检测 fileSuffix 是否为有效扩展名 +if (item.fileSuffix && item.fileSuffix.length <= 5 && /^[a-zA-Z]+$/.test(item.fileSuffix)) { + // 使用 fileSuffix + realExtension = item.fileSuffix.toLowerCase(); + fileType = this.getFileTypeBySuffix(realExtension); +} else { + // 默认按图片处理(违纪证明材料大概率是图片) + realExtension = 'jpg'; + fullFileName = `违纪证明_${timestamp + index}.jpg`; + fileType = 'image/jpeg'; +} +``` + +**局限性**: +- ✅ 可以临时解决图片回显问题 +- ❌ 无法恢复真实的文件名 +- ❌ 非图片文件可能被错误识别 +- ❌ 用户体验不佳 + +--- + +## 验证方法 + +### 1. 后端修复后的验证 + +**步骤**: +1. 上传一张图片 `test.jpg` +2. 调用 `/resource/oss/listByIds/{ossId}` 查询 +3. 检查返回数据: + - `originalName` 应该是 `test.jpg` + - `fileSuffix` 应该是 `jpg` + - `fileName` 应该包含 `.jpg` 扩展名 + +**期望日志**: +``` +违纪记录:原始数据 - originalName=test.jpg, fileSuffix=jpg, fileName=2025/10/24/xxx.jpg +违纪记录:最终文件名=test.jpg, 扩展名=jpg, 文件类型=image/jpeg +``` + +### 2. 前端验证 + +**步骤**: +1. 上传图片后编辑违纪记录 +2. 检查附件回显是否正常 +3. 检查文件名是否正确显示 + +**期望结果**: +- ✅ 图片正常显示(不是灰白背景) +- ✅ 文件名显示为真实文件名(不是UUID) +- ✅ 下载文件名正确 + +--- + +## 相关文件 + +**后端**: +- `backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java` +- `backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java` +- `backend/src/main/java/com/ruoyi/resource/domain/vo/SysOssVo.java` +- `backend/src/main/java/com/ruoyi/resource/domain/SysOss.java` + +**前端**: +- `mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue`(已添加兜底逻辑) + +**数据库**: +- `sys_oss` 表 + +--- + +## 优先级和影响 + +**优先级**: P2(中等) + +**影响评估**: +- **用户体验**: ⬇️ 下降(文件名显示为UUID,无法识别内容) +- **功能可用性**: ✅ 正常(有前端兜底,基本功能可用) +- **数据完整性**: ⚠️ 受影响(历史数据无法恢复真实文件名) + +**建议处理时间**: 2周内修复 + +--- + +**创建时间**: 2025-01-24 +**创建人**: Cursor AI +**状态**: 🔴 待修复 + diff --git a/mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue b/mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue index 64eb75d9..39505066 100644 --- a/mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue +++ b/mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue @@ -519,6 +519,28 @@ @select="handleUploadActionSelect" @close="showUploadActionSheet = false" > + + + + + + @@ -590,6 +612,11 @@ export default { currentViolation: null, currentViolationIndex: -1, + // 删除违纪记录确认对话框 + showDeleteConfirm: false, + deleteViolationStudentId: null, + deleteViolationIndex: -1, + // 附件数据 attachmentList: [], @@ -601,6 +628,10 @@ export default { // 附件上传操作选择框 showUploadActionSheet: false, uploadActions: [{ name: '选择图片' }], + + // 删除附件确认弹窗 + showDeleteImageConfirm: false, + deleteImageTarget: null, }; }, computed: { @@ -712,6 +743,10 @@ export default { await this.checkWechatEnvironment(); this.initComponent(); }, + beforeDestroy() { + // 清理blob URL,防止内存泄漏 + this.cleanupBlobUrls(); + }, methods: { // 检查微信环境并自动初始化SDK async checkWechatEnvironment() { @@ -881,9 +916,425 @@ export default { }, // 加载编辑数据 - loadEditData() { - // TODO: 加载现有检查数据进行编辑 - console.log('加载编辑数据:', this.existingData); + async loadEditData() { + console.log('📝 加载编辑数据:', this.existingData); + + if (!this.existingData) { + console.warn('⚠️ 没有现有数据'); + return; + } + + try { + // 1️⃣ 回显表单基础数据 + this.formData.disciplineTypeId = this.existingData.disciplineTypeId || null; + this.formData.disciplineTypeName = this.existingData.disciplineTypeName || ''; + this.formData.totalScore = this.existingData.totalScore || 0; + this.formData.hygieneGrade = this.existingData.hygieneGrade || 'A'; + this.formData.checkRemarks = this.existingData.checkRemarks || ''; + this.formData.attachmentIds = this.existingData.attachmentIds || ''; + + console.log('✅ 表单基础数据回显完成:', this.formData); + + // 2️⃣ 回显附件数据 + if (this.existingData.attachmentIds) { + await this.loadAttachments(this.existingData.attachmentIds); + } + + // 3️⃣ 从后端接口加载学生评分数据、违纪记录和请假信息(复用Web端接口) + // 注意:loadStudentScoresFromAPI 内部会自动调用 loadViolationRecordsFromAPI 和 loadStudentLeaves + if (this.existingData.id) { + await this.loadStudentScoresFromAPI(this.existingData.id); + } + + console.log('✅ 所有数据回显完成'); + } catch (error) { + console.error('❌ 加载编辑数据失败:', error); + uni.showToast({ title: '加载数据失败', icon: 'none' }); + } + }, + + // 从后端API加载学生评分数据(复用Web端接口) + async loadStudentScoresFromAPI(checkId) { + if (!checkId) { + console.warn('⚠️ 没有检查ID,无法加载学生评分数据'); + return; + } + + this.studentsLoading = true; + try { + console.log('🔄 从API加载学生评分数据,检查ID:', checkId); + + const res = await request({ + url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${checkId}`, + method: 'GET', + }); + + if (res.code === 200 && res.data && res.data.length > 0) { + console.log('✅ 成功获取学生评分数据:', res.data.length, '条'); + + // 将后端返回的学生评分数据映射到roomStudents + this.roomStudents = res.data.map((studentScore) => ({ + // 学生基本信息 + studentId: studentScore.studentId, + studentName: studentScore.studentName, + studentNo: studentScore.studentNo, + className: studentScore.className, + majorName: studentScore.majorName, + collegeName: studentScore.collegeName, + bedId: studentScore.bedId, + bedName: studentScore.bedCode, + bedCode: studentScore.bedCode, + gender: studentScore.gender || '1', + // 评分数据 + student_score: studentScore.studentScore || 0, + attendance_status: studentScore.attendanceStatus || '1', + student_grade: studentScore.studentGrade || 'A', + isResponsiblePerson: studentScore.isResponsiblePerson || 0, + scoreRemarks: studentScore.scoreRemarks || '', + improvementNotes: studentScore.improvementNotes || '', + // UI状态 + showAttendancePicker: false, + showGradePicker: false, + })); + + console.log('✅ 学生评分数据映射完成'); + + // 🔴 同时加载违纪记录和请假信息(复用Web端逻辑) + await Promise.all([ + this.loadViolationRecordsFromAPI(checkId), + this.loadStudentLeaves(), + ]); + } else { + console.warn('⚠️ 未获取到学生评分数据,保持当前学生列表'); + } + } catch (error) { + console.error('❌ 加载学生评分数据失败:', error); + uni.showToast({ title: '加载学生评分数据失败', icon: 'none' }); + } finally { + this.studentsLoading = false; + } + }, + + // 从后端API加载违纪记录数据(复用Web端接口) + async loadViolationRecordsFromAPI(checkId) { + if (!checkId) { + console.warn('⚠️ 没有检查ID,无法加载违纪记录数据'); + return; + } + + try { + console.log('🔄 从API加载违纪记录数据,检查ID:', checkId); + + const res = await request({ + url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`, + method: 'GET', + }); + + if (res.code === 200 && res.data) { + console.log('✅ 成功获取违纪记录数据:', res.data.length, '条'); + + // 清空现有违纪记录 + this.violationRecords.clear(); + + // 按学生ID分组违纪记录 + res.data.forEach((violation) => { + const studentId = violation.studentId; + if (!studentId) return; + + const violations = this.violationRecords.get(studentId) || []; + violations.push({ + disciplineTypeId: violation.disciplineTypeId, + disciplineTypeName: violation.disciplineTypeName, + scoreDeduction: violation.scoreDeduction, + violationDescription: + violation.disciplineContent || violation.disciplineReason || '', + // 🔴 统一使用 attachmentOssIds 字段名 + attachmentOssIds: violation.attachmentOssIds || '', + }); + + this.violationRecords.set(studentId, violations); + console.log( + `✅ 学生ID ${studentId} 违纪记录加载完成,数量:`, + violations.length, + ); + }); + + console.log( + '✅ 所有违纪记录数据加载完成,总学生数:', + this.violationRecords.size, + ); + } else { + console.warn('⚠️ 未获取到违纪记录数据'); + } + } catch (error) { + console.error('❌ 加载违纪记录数据失败:', error); + // 不显示错误提示,因为可能确实没有违纪记录 + } + }, + + // 加载附件数据 + async loadAttachments(attachmentIds) { + if (!attachmentIds) { + console.log('📎 没有附件数据'); + return; + } + + try { + console.log('📎 开始加载附件数据, IDs:', attachmentIds); + + const ossIds = attachmentIds.toString().trim(); + const response = await request({ + url: `/resource/oss/listByIds/${ossIds}`, + method: 'GET', + }); + + if (response.code === 200 && response.data) { + console.log('📎 附件数据加载成功,数量:', response.data.length); + + const attachmentFiles = response.data.map((item, index) => { + // 使用fileSuffix字段判断文件类型 + const fileType = this.getFileTypeBySuffix(item.fileSuffix); + const isImage = this.isImageFile({ + name: item.originalName, + type: fileType, + }); + + // 处理图片URL - 确保有正确的API前缀 + let processedUrl = item.url; + if (isImage && item.url && item.url.startsWith('/resource/')) { + // 图片文件需要添加/api前缀 + processedUrl = `/api${item.url}`; + } + + // 构造完整的文件名(包含扩展名) + let fullFileName = item.originalName || `附件${index + 1}`; + if ( + item.fileSuffix && + !fullFileName + .toLowerCase() + .endsWith(`.${item.fileSuffix.toLowerCase()}`) + ) { + fullFileName = `${fullFileName}.${item.fileSuffix}`; + } + + const fileObj = { + uid: `existing_${index}_${Date.now()}`, + name: fullFileName, + url: processedUrl, + ossId: item.ossId, + status: 'success', + type: fileType, + size: item.fileSize || 0, + }; + + console.log(`📎 附件${index + 1}:`, fileObj); + return fileObj; + }); + + this.attachmentList = attachmentFiles; + + // 为图片文件获取blob数据以确保正确显示 + await this.processImageFiles(); + + // 强制刷新组件显示 + this.$nextTick(() => { + // 预加载图片以确保能正确显示 + this.preloadImages(); + }); + + console.log('✅ 附件数据处理完成'); + } else { + console.warn('⚠️ 获取附件信息失败:', response.msg); + this.attachmentList = []; + } + } catch (error) { + console.error('❌ 获取附件失败:', error); + this.attachmentList = []; + } + }, + + // 根据文件后缀获取文件类型 + getFileTypeBySuffix(fileSuffix) { + if (!fileSuffix) return ''; + + // 移除点号,转换为小写 + const ext = fileSuffix.replace('.', '').toLowerCase(); + + const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + if (imageTypes.includes(ext)) { + const mimeTypeMap = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + bmp: 'image/bmp', + webp: 'image/webp', + }; + return mimeTypeMap[ext] || `image/${ext}`; + } + + // 其他文件类型 + const docTypes = { + pdf: 'application/pdf', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }; + + return docTypes[ext] || `file/${ext}`; + }, + + // 处理图片文件,获取blob数据以确保正确显示 + async processImageFiles() { + console.log('=== 处理图片文件 ==='); + const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); + + for (const file of imageFiles) { + if (file.url && !file.blobUrl) { + try { + console.log(`处理图片文件: ${file.name}`); + const blobUrl = await this.getImageBlobUrl(file.url); + if (blobUrl) { + // 更新文件对象,添加blob URL,并标记为图片 + file.blobUrl = blobUrl; + file.url = blobUrl; // 🔴 关键:使用blob URL作为显示URL,解决灰白背景问题 + file.fileType = 'image'; + file.type = + file.type && file.type.startsWith('image') ? file.type : 'image'; + file.isImage = true; + console.log(`图片文件处理成功: ${file.name} - ${blobUrl}`); + } + } catch (error) { + console.error(`处理图片文件失败: ${file.name}`, error); + } + } + } + }, + + // 获取图片blob URL + async getImageBlobUrl(imageUrl) { + try { + console.log(`获取图片blob数据: ${imageUrl}`); + + // 处理图片URL路径 + let apiUrl = imageUrl; + if (imageUrl.startsWith('/api/')) { + // 移除/api前缀,因为request会自动添加baseURL + apiUrl = imageUrl.replace('/api', ''); + } else if (imageUrl.startsWith('/resource/')) { + // 直接使用相对路径 + apiUrl = imageUrl; + } else if (imageUrl.startsWith('http')) { + // 完整URL,需要特殊处理 + console.log('检测到完整URL,直接返回原URL'); + return imageUrl; + } + + console.log(`API请求URL: ${apiUrl}`); + + // 使用request方法获取图片数据 + const response = await request({ + url: apiUrl, + method: 'GET', + responseType: 'arraybuffer', // 设置响应类型为arraybuffer + header: { + isToken: true, + }, + }); + + // 将arraybuffer转换为blob + const blob = new Blob([response], { type: 'image/*' }); + const blobUrl = URL.createObjectURL(blob); + + console.log(`成功创建blob URL: ${blobUrl}`); + return blobUrl; + } catch (error) { + console.error(`获取图片blob数据失败: ${imageUrl}`, error); + // 如果请求失败,返回原始URL作为备用方案 + return imageUrl; + } + }, + + // 预加载图片以确保能正确显示 + async preloadImages() { + console.log('=== 预加载图片 ==='); + const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); + + for (const file of imageFiles) { + if (file.blobUrl || file.url) { + try { + const imgUrl = file.blobUrl || file.url; + console.log(`预加载图片: ${file.name} - ${imgUrl}`); + // 注意:在uni-app中,可能需要使用不同的预加载方式 + // 这里只是简单的日志记录,实际预加载由image标签自动完成 + } catch (error) { + console.error(`预加载图片失败: ${file.name}`, error); + } + } + } + }, + + // 回显学生评分数据 + loadStudentScores(studentScores) { + console.log('👥 回显学生评分数据:', studentScores); + + studentScores.forEach((scoreData) => { + const student = this.roomStudents.find((s) => s.studentId === scoreData.studentId); + if (student) { + student.student_score = scoreData.studentScore || 0; + student.attendance_status = scoreData.attendanceStatus || '1'; + student.student_grade = scoreData.studentGrade || 'A'; + student.scoreRemarks = scoreData.scoreRemarks || ''; + student.improvementNotes = scoreData.improvementNotes || ''; + console.log(`✅ 学生 ${student.studentName} 评分数据回显完成`); + } + }); + }, + + // 回显违纪记录数据 + loadViolationRecords(violationData) { + console.log('⚠️ 回显违纪记录数据:', violationData); + + this.violationRecords.clear(); + + violationData.forEach((violation) => { + const studentId = violation.studentId; + if (!studentId) return; + + const violations = this.violationRecords.get(studentId) || []; + violations.push({ + disciplineTypeId: violation.disciplineTypeId, + disciplineTypeName: violation.disciplineTypeName, + scoreDeduction: violation.scoreDeduction, + violationDescription: violation.violationDescription, + // 🔴 修复字段名不一致问题:统一使用 attachmentOssIds + // 兼容后端可能返回的两种字段名 + attachmentOssIds: + violation.violationAttachmentIds || violation.attachmentOssIds || '', + }); + + this.violationRecords.set(studentId, violations); + console.log( + `✅ 学生ID ${studentId} 违纪记录回显完成,附件IDs:`, + violation.violationAttachmentIds || violation.attachmentOssIds, + ); + }); + }, + + // 清理blob URL,防止内存泄漏 + cleanupBlobUrls() { + console.log('=== 清理blob URL ==='); + this.attachmentList.forEach((file) => { + if (file.blobUrl && file.blobUrl.startsWith('blob:')) { + try { + URL.revokeObjectURL(file.blobUrl); + console.log(`已清理blob URL: ${file.name}`); + } catch (error) { + console.error(`清理blob URL失败: ${file.name}`, error); + } + } + }); }, // 违纪类型确认处理 @@ -1150,39 +1601,109 @@ export default { // 上传文件到服务器 async uploadFileToServer(file) { try { + console.log('🚀 === 开始上传文件到服务器 ==='); + console.log('📁 待上传文件对象:', file); + console.log('📁 文件对象属性:', Object.keys(file)); + + // 更新文件状态 file.status = 'uploading'; // 🔴 关键修复:保存原始文件名,防止被后端UUID覆盖 const originalName = file.name; console.log('📝 上传前文件名:', originalName); + // 确定正确的文件路径 - uview2通常使用url属性 + const filePath = file.url || file.tempFilePath || file.path; + console.log('🎯 文件路径优先级选择:', { + url: file.url, + tempFilePath: file.tempFilePath, + path: file.path, + selected: filePath, + }); + + if (!filePath) { + const error = new Error('文件路径不存在'); + console.error('❌ 上传失败 - 文件路径为空:', file); + file.status = 'error'; + throw error; + } + + // 显示上传进度 + uni.showLoading({ + title: `上传${file.name}...`, + mask: true, + }); + console.log(`💫 开始上传文件: ${file.name}`); + + // 调用智能上传API(自动适配微信环境) + console.log('📡 调用智能上传API...'); const response = await smartUploadFile('/resource/oss/upload', file, { bizType: 'dorm_hygiene_check_attachment', }); - console.log('📡 上传响应:', response); - console.log('🔍 上传后file.name:', file.name); + console.log('📡 上传API响应:', response); + uni.hideLoading(); if (response && response.code === 200) { + // 上传成功 file.url = response.data.url; file.ossId = response.data.ossId; file.status = 'success'; // 🔴 恢复原始文件名,不使用后端返回的fileName(UUID) file.name = originalName; - console.log('✅ 恢复文件名为:', file.name); - uni.showToast({ title: '上传成功', icon: 'success', duration: 1500 }); - } else { - file.status = 'error'; + + console.log('✅ 文件上传成功:', { + fileName: file.name, + ossId: file.ossId, + url: file.url, + }); + uni.showToast({ - title: (response && response.msg) || '上传失败', + title: '上传成功', + icon: 'success', + duration: 1500, + }); + + return response; + } else { + // 上传失败 + file.status = 'error'; + const errorMsg = (response && response.msg) || '上传失败'; + console.error('❌ 上传失败 - API返回错误:', { + response: response, + fileName: file.name, + errorMsg: errorMsg, + }); + + uni.showToast({ + title: errorMsg, + icon: 'none', + duration: 3000, + }); + + throw new Error(errorMsg); + } + } catch (error) { + console.error('❌ 上传文件失败:', error); + file.status = 'error'; + uni.hideLoading(); + + // 如果是已知错误,直接显示 + if (error.message && error.message !== 'undefined') { + uni.showToast({ + title: error.message, + icon: 'none', + duration: 2000, + }); + } else { + uni.showToast({ + title: '上传失败,请重试', icon: 'none', duration: 2000, }); } - } catch (error) { - console.error('上传文件失败:', error); - file.status = 'error'; - uni.showToast({ title: '上传失败', icon: 'none', duration: 2000 }); + + throw error; } }, @@ -1401,41 +1922,47 @@ export default { event.preventDefault(); } - uni.showModal({ - title: '确认删除', - content: '确定要删除这张图片吗?删除后无法恢复。', - confirmColor: '#ff5722', - success: (res) => { - if (res.confirm) { - // 从附件列表中删除 - const attachmentIndex = this.attachmentList.findIndex( - (file) => - file.uid === image.uid || - (file.ossId && file.ossId === image.ossId) || - (file.url && file.url === image.url), - ); + // 🔴 修复:使用 u-modal 组件替代 uni.showModal,解决 z-index 层级冲突 + this.showDeleteImageConfirm = true; + this.deleteImageTarget = { image, index }; + }, - if (attachmentIndex >= 0) { - this.attachmentList.splice(attachmentIndex, 1); - } + // 确认删除图片 + confirmDeleteImage() { + const { image, index } = this.deleteImageTarget; - // 清理blob URL,防止内存泄漏 - if (image.blobUrl && image.blobUrl.startsWith('blob:')) { - URL.revokeObjectURL(image.blobUrl); - } + // 从附件列表中删除 + const attachmentIndex = this.attachmentList.findIndex( + (file) => + file.uid === image.uid || + (file.ossId && file.ossId === image.ossId) || + (file.url && file.url === image.url), + ); - uni.showToast({ - title: '删除成功', - icon: 'success', - }); - } - }, - fail: (err) => { - console.error('删除确认对话框显示失败:', err); - }, + if (attachmentIndex >= 0) { + this.attachmentList.splice(attachmentIndex, 1); + } + + // 清理blob URL,防止内存泄漏 + if (image.blobUrl && image.blobUrl.startsWith('blob:')) { + URL.revokeObjectURL(image.blobUrl); + } + + this.showDeleteImageConfirm = false; + this.deleteImageTarget = null; + + uni.showToast({ + title: '删除成功', + icon: 'success', }); }, + // 取消删除图片 + cancelDeleteImage() { + this.showDeleteImageConfirm = false; + this.deleteImageTarget = null; + }, + // 获取文件名短格式 getShortFileName(fileName) { if (!fileName) return ''; @@ -1574,24 +2101,39 @@ export default { // 删除违纪记录 removeViolationRecord(studentId, index) { - uni.showModal({ - title: '确认删除', - content: '确认删除这条违纪记录吗?', - success: (res) => { - if (res.confirm) { - const violations = this.violationRecords.get(studentId) || []; - violations.splice(index, 1); + // 保存要删除的记录信息 + this.deleteViolationStudentId = studentId; + this.deleteViolationIndex = index; + // 显示确认对话框 + this.showDeleteConfirm = true; + }, - if (violations.length === 0) { - this.violationRecords.delete(studentId); - } else { - this.violationRecords.set(studentId, violations); - } + // 确认删除违纪记录 + confirmDeleteViolation() { + if (this.deleteViolationStudentId !== null && this.deleteViolationIndex >= 0) { + const violations = this.violationRecords.get(this.deleteViolationStudentId) || []; + violations.splice(this.deleteViolationIndex, 1); - uni.showToast({ title: '违纪记录删除成功', icon: 'success' }); - } - }, - }); + if (violations.length === 0) { + this.violationRecords.delete(this.deleteViolationStudentId); + } else { + this.violationRecords.set(this.deleteViolationStudentId, violations); + } + + uni.showToast({ title: '违纪记录删除成功', icon: 'success' }); + } + + // 重置状态 + this.showDeleteConfirm = false; + this.deleteViolationStudentId = null; + this.deleteViolationIndex = -1; + }, + + // 取消删除违纪记录 + cancelDeleteViolation() { + this.showDeleteConfirm = false; + this.deleteViolationStudentId = null; + this.deleteViolationIndex = -1; }, // 获取学生违纪记录 diff --git a/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue b/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue index 2a7ac3c6..502a5e29 100644 --- a/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue +++ b/mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue @@ -312,6 +312,17 @@ @select="handleUploadActionSelect" @close="showUploadActionSheet = false" > + + + @@ -368,7 +379,25 @@ export default { { required: true, message: '请选择违纪类型', trigger: 'change' }, ], disciplineDate: [{ required: true, message: '请选择违纪日期', trigger: 'change' }], - scoreDeduction: [{ required: true, message: '请输入扣分分值', trigger: 'blur' }], + scoreDeduction: [ + { + required: true, + message: '请输入扣分分值', + trigger: 'blur', + validator: (rule, value, callback) => { + // 允许数字0,但不允许null、undefined、空字符串 + if (value === null || value === undefined || value === '') { + callback(new Error('请输入扣分分值')); + } else if (isNaN(value)) { + callback(new Error('扣分分值必须是数字')); + } else if (value < 0) { + callback(new Error('扣分分值不能小于0')); + } else { + callback(); + } + }, + }, + ], disciplineReason: [{ required: true, message: '请输入违纪原因', trigger: 'blur' }], }, @@ -399,6 +428,10 @@ export default { // 附件上传操作选择框 showUploadActionSheet: false, uploadActions: [{ name: '选择图片' }], + + // 删除确认弹窗 + showDeleteConfirm: false, + deleteTarget: null, }; }, computed: { @@ -458,6 +491,10 @@ export default { this.initDialog(); } }, + beforeUnmount() { + // 组件销毁前清理blob URL,防止内存泄漏 + this.cleanupBlobUrls(); + }, methods: { // === 微信SDK相关方法 === @@ -582,8 +619,8 @@ export default { this.allViolationTypes.length, ); - // 初始化表单 - this.initForm(); + // 🔴 关键修复:等待表单初始化完成(包括附件数据加载) + await this.initForm(); console.log('✅ 违纪记录弹窗初始化完成,最终数据量:', this.allViolationTypes.length); }, @@ -656,7 +693,7 @@ export default { }, // 初始化表单数据 - initForm() { + async initForm() { // 先设置默认日期(确保无论什么情况都有默认日期) this.disciplineDateValue = Number(new Date()); this.disciplineDateDisplay = dayjs().format('YYYY-MM-DD'); @@ -717,13 +754,14 @@ export default { }); } - // 如果是编辑模式,处理附件数据 + // 🔴 关键修复:如果是编辑模式,异步等待附件数据加载完成 if (this.violationData.attachmentOssIds) { console.log( '违纪记录:发现附件数据,准备回显:', this.violationData.attachmentOssIds, ); - this.handleAttachmentData(this.violationData.attachmentOssIds); + await this.handleAttachmentData(this.violationData.attachmentOssIds); + console.log('违纪记录:附件数据回显完成'); } else { console.log('违纪记录:编辑模式但无附件数据'); this.attachmentList = []; @@ -763,17 +801,94 @@ export default { if (response.code === 200 && response.data) { console.log('违纪记录:获取到的附件信息:', response.data); - const attachmentFiles = response.data.map((item, index) => ({ - uid: `existing_${index}_${Date.now()}`, - name: item.originalName || `附件${index + 1}`, - url: item.url, - ossId: item.ossId, - status: 'success', - type: this.getFileTypeByName(item.originalName), - })); + const attachmentFiles = response.data.map((item, index) => { + console.log(`违纪记录:处理第${index + 1}个附件:`, item); + console.log( + `违纪记录:原始数据 - originalName=${item.originalName}, fileSuffix=${item.fileSuffix}, fileName=${item.fileName}`, + ); + + // 处理URL路径 + let processedUrl = item.url || ''; + if ( + processedUrl && + !processedUrl.startsWith('http') && + !processedUrl.startsWith('/') + ) { + processedUrl = `/${processedUrl}`; + } + + // 🔴 核心修复:后端数据质量问题导致无法获取真实文件名和扩展名 + // 问题分析: + // 1. originalName 是 UUID(如 12038a7c-48bd-4978-bcbb-1119a201a6e9) + // 2. fileSuffix 是数字字符(如 "9"、"e"、"d") + // 3. fileName 也没有扩展名(如 2025/10/24/aa991fe4d6114995a0562d783cea56139) + // 解决方案:通过 Content-Type 检测文件类型,默认按图片处理 + + // 生成一个友好的文件名(不使用UUID) + const timestamp = new Date().getTime(); + let fullFileName = `附件_${timestamp + index}`; + let realExtension = ''; + let fileType = ''; + + // 尝试从 fileSuffix 判断(如果是有效的扩展名) + if ( + item.fileSuffix && + item.fileSuffix.length <= 5 && + /^[a-zA-Z]+$/.test(item.fileSuffix) + ) { + realExtension = item.fileSuffix.toLowerCase(); + fullFileName = `附件_${timestamp + index}.${realExtension}`; + fileType = this.getFileTypeBySuffix(realExtension); + console.log( + `违纪记录:从fileSuffix识别 - 扩展名=${realExtension}, 类型=${fileType}`, + ); + } + + // 如果fileSuffix无效,默认按图片处理(因为是违纪证明材料,大概率是图片) + if (!fileType || !fileType.startsWith('image/')) { + console.warn(`违纪记录:无法识别文件类型,默认按jpg图片处理`); + realExtension = 'jpg'; + fullFileName = `违纪证明_${timestamp + index}.jpg`; + fileType = 'image/jpeg'; + } + + console.log( + `违纪记录:最终文件名=${fullFileName}, 扩展名=${realExtension}, 文件类型=${fileType}`, + ); + + const fileObj = { + uid: `existing_${index}_${Date.now()}`, + name: fullFileName, + url: processedUrl, + ossId: item.ossId, + status: 'success', + type: fileType, + size: item.fileSize || 0, + // 为uview2组件添加必要的属性 + file: { + name: fullFileName, + type: fileType, + size: item.fileSize || 0, + path: processedUrl, + url: processedUrl, + }, + }; + + console.log(`违纪记录:构造的文件对象:`, fileObj); + return fileObj; + }); this.attachmentList = attachmentFiles; console.log('违纪记录:设置附件列表成功:', this.attachmentList); + + // 为图片文件获取blob数据以确保正确显示 + await this.processImageFiles(); + + // 强制刷新组件显示 + this.$nextTick(() => { + // 预加载图片以确保能正确显示 + this.preloadImages(); + }); } else { console.warn('违纪记录:获取附件信息失败:', response.msg); this.attachmentList = []; @@ -883,10 +998,19 @@ export default { // 扣分变化处理 handleScoreChange() { + // 确保转换为数字类型 const score = parseFloat(this.formData.scoreDeduction); - if (!isNaN(score) && score < 0) { + if (isNaN(score) || this.formData.scoreDeduction === '') { + // 如果不是有效数字或为空,保持为空字符串或设为0 + return; + } + + if (score < 0) { uni.showToast({ title: '扣分分数不能小于0', icon: 'none' }); this.formData.scoreDeduction = 0; + } else { + // 确保是数字类型 + this.formData.scoreDeduction = score; } }, @@ -1453,38 +1577,47 @@ export default { event.preventDefault(); } - uni.showModal({ - title: '确认删除', - content: '确定要删除这张图片吗?删除后无法恢复。', - confirmColor: '#ff5722', - success: (res) => { - if (res.confirm) { - // 从附件列表中查找并删除 - const attachmentIndex = this.attachmentList.findIndex( - (file) => - file.uid === image.uid || - (file.ossId && file.ossId === image.ossId) || - (file.url && file.url === image.url), - ); + // 🔴 修复:使用 u-modal 组件替代 uni.showModal,解决 z-index 层级冲突 + this.showDeleteConfirm = true; + this.deleteTarget = { image, index }; + }, - if (attachmentIndex >= 0) { - this.attachmentList.splice(attachmentIndex, 1); - } + // 确认删除图片 + confirmDelete() { + const { image, index } = this.deleteTarget; - // 释放blob URL(如果存在) - if (image.blobUrl && image.blobUrl.startsWith('blob:')) { - URL.revokeObjectURL(image.blobUrl); - } + // 从附件列表中查找并删除 + const attachmentIndex = this.attachmentList.findIndex( + (file) => + file.uid === image.uid || + (file.ossId && file.ossId === image.ossId) || + (file.url && file.url === image.url), + ); - uni.showToast({ - title: '删除成功', - icon: 'success', - }); - } - }, + if (attachmentIndex >= 0) { + this.attachmentList.splice(attachmentIndex, 1); + } + + // 释放blob URL(如果存在) + if (image.blobUrl && image.blobUrl.startsWith('blob:')) { + URL.revokeObjectURL(image.blobUrl); + } + + this.showDeleteConfirm = false; + this.deleteTarget = null; + + uni.showToast({ + title: '删除成功', + icon: 'success', }); }, + // 取消删除 + cancelDelete() { + this.showDeleteConfirm = false; + this.deleteTarget = null; + }, + // 获取文件名短格式 getShortFileName(fileName) { if (!fileName) return ''; @@ -1524,6 +1657,165 @@ export default { return `file/${ext}`; }, + // 根据文件后缀获取文件类型 + getFileTypeBySuffix(fileSuffix) { + if (!fileSuffix) return ''; + + // 移除点号,转换为小写 + const ext = fileSuffix.replace('.', '').toLowerCase(); + + // 图片类型判断 + const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']; + if (imageTypes.includes(ext)) { + const mimeTypeMap = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + bmp: 'image/bmp', + webp: 'image/webp', + }; + return mimeTypeMap[ext] || `image/${ext}`; + } + + // 文档类型判断 + const documentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt']; + if (documentTypes.includes(ext)) { + return `file/${ext}`; + } + + return `file/${ext}`; + }, + + // 获取图片的blob数据(用于图片显示) + async getImageBlobUrl(imageUrl) { + try { + console.log(`获取图片blob数据: ${imageUrl}`); + + // 处理图片URL路径 + let apiUrl = imageUrl; + if (imageUrl.startsWith('/api/')) { + // 移除/api前缀,因为request会自动添加baseURL + apiUrl = imageUrl.replace('/api', ''); + } else if (imageUrl.startsWith('/resource/')) { + // 直接使用相对路径 + apiUrl = imageUrl; + } else if (imageUrl.startsWith('http')) { + // 完整URL,需要特殊处理 + console.log('检测到完整URL,直接返回原URL'); + return imageUrl; + } + + console.log(`API请求URL: ${apiUrl}`); + + // 使用request方法获取图片数据 + const response = await request({ + url: apiUrl, + method: 'GET', + responseType: 'arraybuffer', // 设置响应类型为arraybuffer + header: { + isToken: true, + }, + }); + + // 将arraybuffer转换为blob + const blob = new Blob([response], { type: 'image/*' }); + const blobUrl = URL.createObjectURL(blob); + + console.log(`成功创建blob URL: ${blobUrl}`); + return blobUrl; + } catch (error) { + console.error(`获取图片blob数据失败: ${imageUrl}`, error); + // 如果请求失败,返回原始URL作为备用方案 + return imageUrl; + } + }, + + // 处理图片文件,获取blob数据以确保正确显示 + async processImageFiles() { + console.log('=== 处理图片文件 ==='); + console.log('违纪记录:当前附件列表:', this.attachmentList); + + // 检查每个文件是否为图片 + this.attachmentList.forEach((file, index) => { + const isImage = this.isImageFile(file); + console.log( + `违纪记录:文件[${index}] ${file.name}, type=${file.type}, isImage=${isImage}`, + ); + }); + + const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); + console.log(`违纪记录:找到 ${imageFiles.length} 个图片文件`); + + for (const file of imageFiles) { + if (file.url && !file.blobUrl) { + try { + console.log(`违纪记录:开始处理图片: ${file.name}, url=${file.url}`); + const blobUrl = await this.getImageBlobUrl(file.url); + if (blobUrl) { + // 🔴 关键修复:同时更新url和blobUrl字段 + file.blobUrl = blobUrl; + file.url = blobUrl; // 使用blob URL作为显示URL,解决灰白背景问题 + file.fileType = 'image'; + file.type = + file.type && file.type.startsWith('image') ? file.type : 'image'; + file.isImage = true; + console.log(`违纪记录:图片处理成功: ${file.name} - ${blobUrl}`); + } else { + console.warn(`违纪记录:blob URL 为空: ${file.name}`); + } + } catch (error) { + console.error(`违纪记录:处理图片失败: ${file.name}`, error); + } + } else { + console.log( + `违纪记录:跳过处理: ${file.name}, url=${file.url}, blobUrl=${file.blobUrl}`, + ); + } + } + }, + + // 预加载图片以确保能正确显示 + async preloadImages() { + console.log('=== 预加载图片 ==='); + const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); + + for (const file of imageFiles) { + if (file.blobUrl || file.url) { + try { + const imgUrl = file.blobUrl || file.url; + console.log(`预加载图片: ${file.name} - ${imgUrl}`); + // 创建一个Image对象来预加载图片 + const img = new Image(); + img.onload = () => { + console.log(`图片预加载成功: ${file.name}`); + }; + img.onerror = (error) => { + console.error(`图片预加载失败: ${file.name}`, error); + }; + img.src = imgUrl; + } catch (error) { + console.error(`预加载图片异常: ${file.name}`, error); + } + } + } + }, + + // 清理blob URL,防止内存泄漏 + cleanupBlobUrls() { + console.log('=== 清理blob URL ==='); + this.attachmentList.forEach((file) => { + if (file.blobUrl && file.blobUrl.startsWith('blob:')) { + try { + URL.revokeObjectURL(file.blobUrl); + console.log(`已清理blob URL: ${file.name}`); + } catch (error) { + console.error(`清理blob URL失败: ${file.name}`, error); + } + } + }); + }, + // 图片加载错误处理 handleImageError(event) { console.warn('图片加载失败:', event); @@ -1532,11 +1824,21 @@ export default { // 判断是否为图片文件 isImageFile(file) { const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp']; + + // 检查 file.type if (file.type) { - return imageTypes.includes(file.type); + const isImageByType = imageTypes.includes(file.type); + console.log( + `违纪记录:通过type判断: ${file.name}, type=${file.type}, 是否图片=${isImageByType}`, + ); + return isImageByType; } + + // 通过文件名判断 const fileName = file.name || file.url || ''; - return /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName); + const isImageByName = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName); + console.log(`违纪记录:通过文件名判断: ${fileName}, 是否图片=${isImageByName}`); + return isImageByName; }, // 确定