提交代码

This commit is contained in:
zlwu committed 2025-10-24 23:34:48 +08:00
1 parent e97c97aa0b
commit 1b117de6cf
3 files changed
+1246 -102

No files matched your search

@@ -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<SysOss> 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<SysOssVo> listByIds(String ossIds) {
// ... 查询逻辑 ...
List<SysOssVo> 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
**状态**: 🔴 待修复
@@ -519,6 +519,28 @@
@select="handleUploadActionSelect"
@close="showUploadActionSheet = false"
></u-action-sheet>
<!-- 删除违纪记录确认对话框 -->
<u-modal
:show="showDeleteConfirm"
title="确认删除"
content="确认删除这条违纪记录吗?"
:show-cancel-button="true"
confirm-color="#ff5722"
@confirm="confirmDeleteViolation"
@cancel="cancelDeleteViolation"
></u-modal>
<!-- 删除附件确认对话框 -->
<u-modal
:show="showDeleteImageConfirm"
title="确认删除"
content="确定要删除这张图片吗?删除后无法恢复。"
:show-cancel-button="true"
confirm-color="#ff5722"
@confirm="confirmDeleteImage"
@cancel="cancelDeleteImage"
></u-modal>
</view>
</template>
@@ -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;
},
// 获取学生违纪记录
@@ -312,6 +312,17 @@
@select="handleUploadActionSelect"
@close="showUploadActionSheet = false"
></u-action-sheet>
<!-- 删除确认弹窗(使用 u-modal 解决 z-index 层级问题) -->
<u-modal
:show="showDeleteConfirm"
title="确认删除"
content="确定要删除这张图片吗?删除后无法恢复。"
:show-cancel-button="true"
confirm-color="#ff5722"
@confirm="confirmDelete"
@cancel="cancelDelete"
></u-modal>
</u-popup>
</template>
@@ -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;
},
// 确定