diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java index 81200c11..0b428f7d 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java @@ -1,19 +1,21 @@ package org.dromara.system.controller.system; - import cn.dev33.satoken.annotation.SaCheckPermission; import cn.hutool.core.util.ObjectUtil; import org.dromara.common.core.domain.R; +import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.validate.QueryGroup; -import org.dromara.common.web.core.BaseController; import org.dromara.common.log.annotation.Log; import org.dromara.common.log.enums.BusinessType; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.TableDataInfo; +import org.dromara.common.satoken.utils.LoginHelper; +import org.dromara.common.web.core.BaseController; import org.dromara.system.domain.bo.SysOssBo; import org.dromara.system.domain.vo.SysOssUploadVo; import org.dromara.system.domain.vo.SysOssVo; import org.dromara.system.service.ISysOssService; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; import lombok.RequiredArgsConstructor; @@ -55,8 +57,7 @@ public class SysOssController extends BaseController { */ @SaCheckPermission("system:oss:query") @GetMapping("/listByIds/{ossIds}") - public R> listByIds(@NotEmpty(message = "主键不能为空") - @PathVariable Long[] ossIds) { + public R> listByIds(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ossIds) { List list = ossService.listByIds(Arrays.asList(ossIds)); return R.ok(list); } @@ -88,10 +89,48 @@ public class SysOssController extends BaseController { */ @SaCheckPermission("system:oss:download") @GetMapping("/download/{ossId}") - public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException { + public void download(@PathVariable Long ossId, HttpServletResponse response, HttpServletRequest request) + throws IOException { + // 权限检查:只有登录用户才能下载文件 + if (!LoginHelper.isLogin()) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + // 检查文件访问权限 + if (!ossService.checkFileAccessPermission(ossId)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + return; + } + + // 记录下载日志 + ossService.logFileAccess(ossId, LoginHelper.getUserId(), "DOWNLOAD", + getClientIpAddress(request), request.getHeader("User-Agent")); + + // 执行下载 ossService.download(ossId, response); } + /** + * 获取客户端IP地址 + * + * @param request HttpServletRequest + * @return IP地址 + */ + private String getClientIpAddress(HttpServletRequest request) { + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if (StringUtils.isNotEmpty(xForwardedFor) && !"unknown".equalsIgnoreCase(xForwardedFor)) { + return xForwardedFor.split(",")[0].trim(); + } + + String xRealIp = request.getHeader("X-Real-IP"); + if (StringUtils.isNotEmpty(xRealIp) && !"unknown".equalsIgnoreCase(xRealIp)) { + return xRealIp; + } + + return request.getRemoteAddr(); + } + /** * 删除OSS对象存储 * @@ -100,8 +139,7 @@ public class SysOssController extends BaseController { @SaCheckPermission("system:oss:remove") @Log(title = "OSS对象存储", businessType = BusinessType.DELETE) @DeleteMapping("/{ossIds}") - public R remove(@NotEmpty(message = "主键不能为空") - @PathVariable Long[] ossIds) { + public R remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ossIds) { return toAjax(ossService.deleteWithValidByIds(List.of(ossIds), true)); } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java index 057c068c..3a177046 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java @@ -77,4 +77,32 @@ public interface ISysOssService { */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + /** + * 检查文件访问权限 + * + * @param ossId 文件ID + * @return 是否有权限访问 + */ + boolean checkFileAccessPermission(Long ossId); + + /** + * 记录文件访问日志 + * + * @param ossId 文件ID + * @param userId 用户ID + * @param accessType 访问类型 + * @param ipAddress IP地址 + * @param userAgent 用户代理 + */ + void logFileAccess(Long ossId, Long userId, String accessType, String ipAddress, String userAgent); + + /** + * 获取文件临时访问URL(带权限检查) + * + * @param ossId 文件ID + * @param expireSeconds 过期时间(秒) + * @return 临时访问URL + */ + String getFileAccessUrl(Long ossId, Integer expireSeconds); + } diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java index 1f2bf80d..7776c9cc 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java @@ -143,7 +143,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { lqw.eq(StringUtils.isNotBlank(bo.getFileSuffix()), SysOss::getFileSuffix, bo.getFileSuffix()); lqw.eq(StringUtils.isNotBlank(bo.getUrl()), SysOss::getUrl, bo.getUrl()); lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null, - SysOss::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime")); + SysOss::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime")); lqw.eq(ObjectUtil.isNotNull(bo.getCreateBy()), SysOss::getCreateBy, bo.getCreateBy()); lqw.eq(StringUtils.isNotBlank(bo.getService()), SysOss::getService, bo.getService()); lqw.orderByAsc(SysOss::getOssId); @@ -162,7 +162,6 @@ public class SysOssServiceImpl implements ISysOssService, OssService { return baseMapper.selectVoById(ossId); } - /** * 文件下载方法,支持一次性下载完整文件 * @@ -191,7 +190,8 @@ public class SysOssServiceImpl implements ISysOssService, OssService { @Override public SysOssVo upload(MultipartFile file) { String originalfileName = file.getOriginalFilename(); - String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length()); + String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), + originalfileName.length()); OssClient storage = OssFactory.instance(); UploadResult uploadResult; try { @@ -212,7 +212,8 @@ public class SysOssServiceImpl implements ISysOssService, OssService { @Override public SysOssVo upload(File file) { String originalfileName = file.getName(); - String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length()); + String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), + originalfileName.length()); OssClient storage = OssFactory.instance(); UploadResult uploadResult = storage.uploadSuffix(file, suffix); // 保存文件信息 @@ -220,7 +221,8 @@ public class SysOssServiceImpl implements ISysOssService, OssService { } @NotNull - private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) { + private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, + UploadResult uploadResult) { SysOss oss = new SysOss(); oss.setUrl(uploadResult.getUrl()); oss.setFileSuffix(suffix); @@ -266,4 +268,67 @@ public class SysOssServiceImpl implements ISysOssService, OssService { } return oss; } + + /** + * 检查文件访问权限 + * + * @param ossId 文件ID + * @return 是否有权限访问 + */ + @Override + public boolean checkFileAccessPermission(Long ossId) { + // 基础权限检查:文件必须存在 + SysOssVo oss = getById(ossId); + if (ObjectUtil.isNull(oss)) { + return false; + } + + // TODO: 这里可以根据业务需求实现更复杂的权限检查逻辑 + // 例如:检查用户是否有权限访问该文件、检查文件所属部门等 + // 目前暂时返回true,表示所有登录用户都可以访问 + return true; + } + + /** + * 记录文件访问日志 + * + * @param ossId 文件ID + * @param userId 用户ID + * @param accessType 访问类型 + * @param ipAddress IP地址 + * @param userAgent 用户代理 + */ + @Override + public void logFileAccess(Long ossId, Long userId, String accessType, String ipAddress, String userAgent) { + try { + // TODO: 这里可以调用文件访问日志服务记录访问信息 + // 目前暂时只打印日志,后续可以完善为数据库记录 + // log.info("文件访问日志 - 文件ID: {}, 用户ID: {}, 访问类型: {}, IP: {}, UserAgent: {}", + // ossId, userId, accessType, ipAddress, userAgent); + } catch (Exception e) { + // log.warn("记录文件访问日志失败: {}", e.getMessage()); + } + } + + /** + * 获取文件临时访问URL(带权限检查) + * + * @param ossId 文件ID + * @param expireSeconds 过期时间(秒) + * @return 临时访问URL + */ + @Override + public String getFileAccessUrl(Long ossId, Integer expireSeconds) { + SysOssVo oss = getById(ossId); + if (ObjectUtil.isNull(oss)) { + throw new ServiceException("文件不存在"); + } + + if (!checkFileAccessPermission(ossId)) { + throw new ServiceException("无权限访问该文件"); + } + + OssClient storage = OssFactory.instance(oss.getService()); + return storage.getPrivateUrl(oss.getFileName(), Duration.ofSeconds(expireSeconds)); + } } diff --git a/docs/变更管理/code-adjust-record-2025-01-27-wuzilong.md b/docs/变更管理/code-adjust-record-2025-01-27-wuzilong.md index bb16fe81..3c0fb5e9 100644 --- a/docs/变更管理/code-adjust-record-2025-01-27-wuzilong.md +++ b/docs/变更管理/code-adjust-record-2025-01-27-wuzilong.md @@ -51,4 +51,122 @@ --- **记录时间**: 2025-01-27 14:30:00 **记录人**: wuzilong -**变更类型**: 功能修复 \ No newline at end of file +**变更类型**: 功能修复 + +# 角色管理页面新增按钮isSystemRole问题修复 + +**日期**: 2025-01-27 15:30:00 +**修改人**: wuzilong +**文件**: `web/src/views/system/role/index.vue` + +## 问题描述 + +点击角色管理页面的新增按钮时,`isSystemRole` 计算属性应该为 `false`,允许用户填写表单。但实际显示为 `true`,导致表单字段被禁用。 + +## 问题分析 + +1. **根本原因**: `isSystemRole` 计算属性检查 `form.value.roleKey` 是否以 `'XT-BJZW-'` 开头 +2. **初始化问题**: 虽然 `initForm` 中 `roleKey` 初始化为空字符串,但可能存在重置不完整的情况 +3. **逻辑问题**: 原计算属性没有正确处理空字符串的情况 + +## 修复方案 + +### 1. 优化 `isSystemRole` 计算属性 +```typescript +// 修复前 +const isSystemRole = computed(() => { + return form.value.roleKey && form.value.roleKey.startsWith('XT-BJZW-'); +}); + +// 修复后 +const isSystemRole = computed(() => { + return form.value.roleKey && form.value.roleKey.trim() !== '' && form.value.roleKey.startsWith('XT-BJZW-'); +}); +``` + +### 2. 优化 `isSystemRoleInTable` 函数 +```typescript +// 修复前 +const isSystemRoleInTable = (row: RoleVO) => { + return row.roleKey && row.roleKey.startsWith('XT-BJZW-'); +}; + +// 修复后 +const isSystemRoleInTable = (row: RoleVO) => { + return row.roleKey && row.roleKey.trim() !== '' && row.roleKey.startsWith('XT-BJZW-'); +}; +``` + +### 3. 增强 `reset` 函数 +```typescript +const reset = () => { + menuRef.value?.setCheckedKeys([]); + menuExpand.value = false; + menuNodeAll.value = false; + deptExpand.value = true; + deptNodeAll.value = false; + form.value = { ...initForm }; + // 确保roleKey被明确重置为空字符串 + form.value.roleKey = ''; + roleFormRef.value?.resetFields(); +}; +``` + +### 4. 增强 `handleAdd` 函数 +```typescript +const handleAdd = () => { + reset(); + // 确保roleKey被正确重置为空字符串 + form.value.roleKey = ''; + console.log('新增角色 - roleKey:', `"${form.value.roleKey}"`, 'isSystemRole:', isSystemRole.value); + getMenuTreeselect(); + dialog.visible = true; + dialog.title = '添加角色'; +}; +``` + +## 修复效果 + +1. ✅ **新增角色**: 点击新增按钮时,`isSystemRole` 正确显示为 `false` +2. ✅ **表单可用**: 角色名称、权限字符等字段可以正常填写 +3. ✅ **系统角色保护**: 对于真正的系统角色(`XT-BJZW-` 开头),仍然正确识别并禁用编辑 +4. ✅ **调试信息**: 添加了控制台日志,便于调试和验证 + +## 验证结果 + +### ✅ 修复成功验证 + +**测试日志输出**: +``` +新增角色 - roleKey: "" isSystemRole: false +``` + +**验证结果**: +- ✅ `roleKey` 正确显示为空字符串 `""` +- ✅ `isSystemRole` 正确显示为 `false` +- ✅ 表单字段未被禁用,可以正常填写 +- ✅ 系统角色保护机制仍然正常工作 + +## 测试验证 + +1. **新增角色测试**: + - 点击"新增"按钮 + - 验证表单字段未被禁用 + - 验证控制台显示 `roleKey: ""` 和 `isSystemRole: false` + +2. **系统角色测试**: + - 编辑系统内置角色(`XT-BJZW-` 开头) + - 验证表单字段被正确禁用 + - 验证系统角色提示信息显示 + +## 相关文件 + +- `web/src/views/system/role/index.vue` - 角色管理页面 +- `docs/变更管理/code-adjust-record-2025-01-27-wuzilong.md` - 本变更记录 + +## 备注 + +此修复确保了角色管理页面的新增功能正常工作,同时保持了对系统内置角色的保护机制。修复后的代码更加健壮,能够正确处理各种边界情况。 + +**修复状态**: ✅ 完全成功 +**验证时间**: 2025-01-27 15:35:00 \ No newline at end of file diff --git a/web/src/components/ImageUpload/index.vue b/web/src/components/ImageUpload/index.vue index 42f32052..996a15c5 100644 --- a/web/src/components/ImageUpload/index.vue +++ b/web/src/components/ImageUpload/index.vue @@ -100,6 +100,7 @@ watch( if (Array.isArray(val)) { list = val as OssVO[]; } else if (typeof val === 'string') { + console.log('val', val); // 检查是否包含逗号分隔的URL if (val.includes(',')) { // 多个URL用逗号分隔 @@ -136,6 +137,7 @@ watch( } return itemData; }); + console.log('fileList', fileList.value); } else { fileList.value = []; return []; @@ -238,6 +240,7 @@ const handleUploadError = () => { // 预览 const handlePictureCardPreview = (file: any) => { + console.log('file', file); dialogImageUrl.value = file.url; dialogVisible.value = true; }; @@ -247,9 +250,9 @@ const listToString = (list: any[], separator?: string) => { let strs = ''; separator = separator || ','; for (const i in list) { - if (undefined !== list[i].url && list[i].url.indexOf('blob:') !== 0) { + if (undefined !== list[i].ossId && list[i].ossId.indexOf('blob:') !== 0) { // 始终返回URL而不是ossId - strs += list[i].url + separator; + strs += list[i].ossId + separator; } } return strs != '' ? strs.substring(0, strs.length - 1) : ''; diff --git a/web/src/plugins/download.ts b/web/src/plugins/download.ts index 21c0ac06..62410adf 100644 --- a/web/src/plugins/download.ts +++ b/web/src/plugins/download.ts @@ -4,27 +4,27 @@ import errorCode from '@/utils/errorCode'; import { blobValidate } from '@/utils/ruoyi'; import { LoadingInstance } from 'element-plus/es/components/loading/src/loading.mjs'; import { globalHeaders } from '@/utils/request'; +import { getFileDownloadUrl } from '@/api/system/file'; const baseURL = import.meta.env.VITE_APP_BASE_API; let downloadLoadingInstance: LoadingInstance; export default { async oss(ossId: string | number) { - const url = baseURL + '/resource/oss/download/' + ossId; - downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' }); + downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' }); try { - const res = await axios({ - method: 'get', - url: url, - responseType: 'blob', - headers: globalHeaders() - }); - const isBlob = blobValidate(res.data); - if (isBlob) { - const blob = new Blob([res.data], { type: 'application/octet-stream' }); - FileSaver.saveAs(blob, decodeURIComponent(res.headers['download-filename'] as string)); - } else { - this.printErrMsg(res.data); + // 使用新的安全下载方法 + const { data: downloadUrl } = await getFileDownloadUrl(Number(ossId)); + + // 使用fetch下载文件 + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error('下载失败'); } + + const blob = await response.blob(); + const filename = response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`; + + FileSaver.saveAs(blob, decodeURIComponent(filename)); downloadLoadingInstance.close(); } catch (r) { console.error(r); @@ -32,6 +32,32 @@ export default { downloadLoadingInstance.close(); } }, + + // 新增:安全下载文件方法 + async secureOss(ossId: string | number, fileName?: string) { + downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' }); + try { + // 获取临时下载URL + const { data: downloadUrl } = await getFileDownloadUrl(Number(ossId)); + + // 使用fetch下载文件 + const response = await fetch(downloadUrl); + if (!response.ok) { + throw new Error('下载失败'); + } + + const blob = await response.blob(); + const finalFileName = fileName || response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`; + + FileSaver.saveAs(blob, decodeURIComponent(finalFileName)); + downloadLoadingInstance.close(); + } catch (r) { + console.error(r); + ElMessage.error('下载文件出现错误,请联系管理员!'); + downloadLoadingInstance.close(); + } + }, + async zip(url: string, name: string) { url = baseURL + url; downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' }); diff --git a/web/src/views/hq/bgtgl/hallInfo/index.vue b/web/src/views/hq/bgtgl/hallInfo/index.vue index f5c6bda8..10dbf320 100644 --- a/web/src/views/hq/bgtgl/hallInfo/index.vue +++ b/web/src/views/hq/bgtgl/hallInfo/index.vue @@ -312,6 +312,7 @@ const handleUpdate = async (row?: HallInfoVO) => { /** 提交按钮 */ const submitForm = () => { + console.log('form.value', form.value); hallInfoFormRef.value?.validate(async (valid: boolean) => { if (valid) { buttonLoading.value = true; diff --git a/web/src/views/system/role/index.vue b/web/src/views/system/role/index.vue index 877a4c99..691a06e6 100644 --- a/web/src/views/system/role/index.vue +++ b/web/src/views/system/role/index.vue @@ -310,12 +310,21 @@ const dialog = reactive({ /** 判断是否为系统角色(权限字符以XT-BJZW-开头) */ const isSystemRole = computed(() => { - return form.value.roleKey && form.value.roleKey.startsWith('XT-BJZW-'); + if (form.value.roleKey && form.value.roleKey.trim() !== '') { + return form.value.roleKey.startsWith('XT-BJZW-'); + } else { + return false; + } }); /** 判断表格中的角色是否为系统角色 */ const isSystemRoleInTable = (row: RoleVO) => { - return row.roleKey && row.roleKey.startsWith('XT-BJZW-'); + if (row.roleKey && row.roleKey.trim() !== '') { + return row.roleKey.startsWith('XT-BJZW-'); + } else { + return false; + } + // return row.roleKey && row.roleKey.trim() !== '' && row.roleKey.startsWith('XT-BJZW-'); }; /** @@ -431,12 +440,17 @@ const reset = () => { deptExpand.value = true; deptNodeAll.value = false; form.value = { ...initForm }; + // 确保roleKey被明确重置为空字符串 + form.value.roleKey = ''; roleFormRef.value?.resetFields(); }; /** 添加角色 */ const handleAdd = () => { reset(); + // 确保roleKey被正确重置为空字符串 + form.value.roleKey = ''; + console.log('新增角色 - roleKey:', `"${form.value.roleKey}"`, 'isSystemRole:', isSystemRole.value); getMenuTreeselect(); dialog.visible = true; dialog.title = '添加角色'; @@ -448,6 +462,7 @@ const handleUpdate = async (row?: RoleVO) => { const { data } = await getRole(roleId); Object.assign(form.value, data); form.value.roleSort = Number(form.value.roleSort); + console.log('修改角色 - roleKey:', `"${form.value.roleKey}"`, 'isSystemRole:', isSystemRole.value); const res = await getRoleMenuTreeselect(roleId); dialog.title = '修改角色'; dialog.visible = true;