zlwu 1 год назад
Родитель
Сommit
2b5db607f6

+ 45 - 7
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<List<SysOssVo>> listByIds(@NotEmpty(message = "主键不能为空")
-                                       @PathVariable Long[] ossIds) {
+    public R<List<SysOssVo>> listByIds(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ossIds) {
         List<SysOssVo> 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<Void> remove(@NotEmpty(message = "主键不能为空")
-                          @PathVariable Long[] ossIds) {
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ossIds) {
         return toAjax(ossService.deleteWithValidByIds(List.of(ossIds), true));
     }
 

+ 28 - 0
backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java

@@ -77,4 +77,32 @@ public interface ISysOssService {
      */
     Boolean deleteWithValidByIds(Collection<Long> 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);
+
 }

+ 70 - 5
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));
+    }
 }

+ 119 - 1
docs/变更管理/code-adjust-record-2025-01-27-wuzilong.md

@@ -51,4 +51,122 @@
 ---
 **记录时间**: 2025-01-27 14:30:00  
 **记录人**: wuzilong  
-**变更类型**: 功能修复 
+**变更类型**: 功能修复 
+
+# 角色管理页面新增按钮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 

+ 5 - 2
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) : '';

+ 40 - 14
web/src/plugins/download.ts

@@ -4,27 +4,52 @@ 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);
+      ElMessage.error('下载文件出现错误,请联系管理员!');
+      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);
@@ -32,6 +57,7 @@ export default {
       downloadLoadingInstance.close();
     }
   },
+
   async zip(url: string, name: string) {
     url = baseURL + url;
     downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });

+ 1 - 0
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;

+ 17 - 2
web/src/views/system/role/index.vue

@@ -310,12 +310,21 @@ const dialog = reactive<DialogOption>({
 
 /** 判断是否为系统角色(权限字符以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;