Browse Source

Squashed commit of the following:

commit 3faeaa79cd9175c694a8ef217c95b0f46aa30cd7
Author: zhangzl <123456789Aa>
Date:   Mon Nov 17 11:08:56 2025 +0900

    支持在启动时选择使用Redis或数据库作为缓存存储方案。
huning 2 months ago
parent
commit
f36367147d
24 changed files with 1410 additions and 85 deletions
  1. 7 0
      new-react-admin-ui/package-lock.json
  2. 1 0
      new-react-admin-ui/package.json
  3. 280 0
      new-react-admin-ui/src/pages/dashboard/OrderIndex.tsx
  4. 5 0
      new-react-admin-ui/yarn.lock
  5. 1 10
      ruoyi-admin/pom.xml
  6. 3 0
      ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
  7. 3 3
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
  8. 5 5
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
  9. 24 22
      ruoyi-admin/src/main/resources/application.yml
  10. 25 0
      ruoyi-common/src/main/java/com/ruoyi/common/config/AppConfig.java
  11. 9 0
      ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java
  12. 392 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/cache/AppCache.java
  13. 491 0
      ruoyi-common/src/main/java/com/ruoyi/common/core/cache/DbCache.java
  14. 1 1
      ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java
  15. 6 6
      ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java
  16. 15 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/JacksonUtils.java
  17. 5 6
      ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java
  18. 4 4
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
  19. 6 6
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java
  20. 4 4
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
  21. 5 5
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
  22. 19 13
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java
  23. 67 0
      ruoyi-system/src/main/resources/mapper/system/SysOrderMapper.xml
  24. 32 0
      sql/sys_cache.sql

+ 7 - 0
new-react-admin-ui/package-lock.json

@@ -14,6 +14,7 @@
         "@mui/material": "^6.1.9",
         "@mui/x-tree-view": "^8.16.0",
         "@types/antd": "^0.12.32",
+        "ag-grid": "^18.1.2",
         "ag-grid-enterprise": "^34.3.1",
         "ag-grid-react": "^34.3.1",
         "antd": "^5.28.1",
@@ -1992,6 +1993,12 @@
       "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-12.3.1.tgz",
       "integrity": "sha512-5216xYoawnvMXDFI6kTpPku+mH0Csiwu/FE7lsAm8Z22HEN6ciSG/V7g+IrpLWncELqksgENebCTP75PZ3CsHA=="
     },
+    "node_modules/ag-grid": {
+      "version": "18.1.2",
+      "resolved": "https://registry.npmjs.org/ag-grid/-/ag-grid-18.1.2.tgz",
+      "integrity": "sha512-HtJt8iFcRKCBj5UHBDmwSLLr72F3XDACeBNarH4nJWFHIqcnu7u0Ifrd2nftPmfEBj6YjFHawDqcZL2yo3YfmQ==",
+      "deprecated": "ag-grid is now deprecated - please use @ag-grid-community/all-modules. See www.ag-grid.com/javascript-grid-modules/ for more information."
+    },
     "node_modules/ag-grid-community": {
       "version": "34.3.1",
       "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-34.3.1.tgz",

+ 1 - 0
new-react-admin-ui/package.json

@@ -17,6 +17,7 @@
     "@mui/material": "^6.1.9",
     "@mui/x-tree-view": "^8.16.0",
     "@types/antd": "^0.12.32",
+    "ag-grid": "^18.1.2",
     "ag-grid-enterprise": "^34.3.1",
     "ag-grid-react": "^34.3.1",
     "antd": "^5.28.1",

+ 280 - 0
new-react-admin-ui/src/pages/dashboard/OrderIndex.tsx

@@ -0,0 +1,280 @@
+import React, {
+    useCallback,
+    useMemo,
+    useRef,
+    useState,
+    useEffect
+} from "react";
+import { AgGridReact } from "ag-grid-react";
+import {
+    ClientSideRowModelModule,
+    ColDef,
+    ModuleRegistry,
+    TextFilterModule,
+    ValidationModule,
+    INumberCellEditorParams,
+    ISetFilterParams,
+    NumberEditorModule,
+} from "ag-grid-community";
+import { RowGroupingModule, SetFilterModule } from "ag-grid-enterprise";
+// import { IOlympicData } from "./interfaces";
+
+import { useNavigate } from 'react-router-dom'; // 改用 react-router-dom 的 useNavigate
+
+// 使用项目中封装的认证request函数,确保自动携带token
+import { request } from '@/services/permissionService';
+// 若项目未安装 antd,则临时用 console.warn 代替 message
+let message: any;
+try {
+  message = require('antd').message;
+} catch {
+  message = {
+    error: (msg: string) => console.warn('[antd-message mock]', msg),
+  };
+}
+// 若项目未提供 clearSessionToken,则临时用 localStorage 清除 token
+let clearSessionToken: () => void;
+try {
+  clearSessionToken = require('@/utils/access').clearSessionToken;
+} catch {
+  clearSessionToken = () => {
+    localStorage.removeItem('token');
+    localStorage.removeItem('sessionToken');
+  };
+}
+
+// 注册必要的模块
+ModuleRegistry.registerModules([
+    ClientSideRowModelModule,
+    TextFilterModule,
+    RowGroupingModule,
+    SetFilterModule,
+    NumberEditorModule,
+    ...(process.env.NODE_ENV !== "production" ? [ValidationModule] : []),
+]);
+
+export interface IOlympicData {
+    depName: string,
+    regName: string,
+    baseCode: string,
+    baseName: string,
+    shipNumber: number,
+    linkCap: number,
+    fixedCap: number,
+    // indivConfig: number,
+}
+
+const GridExample = () => {
+    const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
+    const [rowData, setRowData] = useState<IOlympicData[]>([]); // 改为状态管理
+    const [loading, setLoading] = useState<boolean>(true); // 加载状态
+    const [error, setError] = useState<string | null>(null); // 错误状态
+    // 获取 navigate 函数
+    const navigate = useNavigate();
+    
+    // 处理"設定"点击,跳转并传递行数据
+    const handleConfigClick = useCallback((rowData: IOlympicData) => {
+        const { depName, regName, baseCode, baseName } = rowData;
+        // 跳转到详情页(需提前配置路由,如"/config/detail")
+        navigate({
+            pathname: '/base/master',
+            search: `?depName=${depName}&regName=${regName}&baseCode=${baseCode}&baseName=${baseName}`,
+        });
+    }, [navigate]);
+
+    // 从后端获取数据
+    useEffect(() => {
+
+        const fetchData = async () => {
+            try {
+                setLoading(true);
+                
+                // 使用项目中封装的认证request函数,确保自动携带token
+                const response = await request('/system/order/orderselect', {
+                    method: 'GET',
+                    headers: {
+                        'Content-Type': 'application/json;charset=UTF-8',
+                    },
+                });
+
+                console.log('Received data:', response);
+
+                // 处理若依后台返回的标准格式 {code: 200, msg: 'xxx', data: [...]}
+                // 如果response有data字段,使用data;否则使用response本身
+                const data = (response as any)?.data || (Array.isArray(response) ? response : []);
+
+                console.log('Data type:', typeof data);
+                console.log('Is array?', Array.isArray(data));
+                console.log('Data length:', data.length);
+
+                // 检查第一条数据的结构
+                if (data.length > 0) {
+                    console.log('First item structure:', data[0]);
+                    console.log('Available fields:', Object.keys(data[0]));
+                }
+
+                setRowData(data);
+                setError(null);
+            } catch (err) {
+                const errorMessage = err instanceof Error ? err.message : '发生未知错误';
+                setError(errorMessage);
+                console.error('获取数据错误:', err);
+
+                // 处理401错误,跳转到登录页面
+                if (errorMessage.includes('500')) {
+                    message.error('服务器处理失败,请联系管理员检查');
+                }
+                if (errorMessage.includes('401')) {
+                    message.error('登录已过期,请重新登录');
+                    clearSessionToken();
+                    setTimeout(() => {
+                        navigate('/user/login');
+                    }, 1000);
+                }
+            } finally {
+                setLoading(false);
+            }
+        };
+
+        fetchData();
+    }, []);
+
+    // 处理单元格编辑后的值变化
+    const onCellValueChanged = useCallback((params: any) => {
+        if (params.colDef.field === 'fixedCap') {
+            console.log(`確定キャップ修改: 旧值=${params.oldValue}, 新值=${params.newValue}`);
+            // 可以在这里添加保存到后端的逻辑
+            // fetch(`/api/update-cap/${params.data.baseCode}`, {
+            //   method: 'PUT',
+            //   headers: { 'Content-Type': 'application/json' },
+            //   body: JSON.stringify({ fixedCap: params.newValue })
+            // })
+        }
+    }, []);
+
+    const [columnDefs, setColumnDefs] = useState<ColDef[]>([
+        // 列定义保持不变(省略,同原代码)
+        {
+            headerName: "部名",
+            field: "depName",
+            rowGroup: true,
+            rowGroupIndex: 0,
+            hide: true,
+            filter: true,
+            filterParams: { buttons: ['apply', 'clear'] }
+        },
+        {
+            headerName: "地区名",
+            field: "regName",
+            rowGroup: true,
+            rowGroupIndex: 1,
+            hide: true,
+            filter: true,
+            filterParams: { buttons: ['apply', 'clear'] }
+        },
+        {
+            headerName: "拠点コード",
+            field: "baseCode",
+            filter: 'agSetColumnFilter',
+            filterParams: {
+                buttons: ['apply', 'clear'],
+                applyMiniFilterWhileTyping: true,
+                suppressSorting: true,
+            } as ISetFilterParams,
+        },
+        {
+            headerName: "拠点名",
+            field: "baseName",
+            filter: 'agSetColumnFilter',
+            filterParams: {
+                buttons: ['apply', 'clear'],
+                applyMiniFilterWhileTyping: true,
+                suppressSorting: true,
+            } as ISetFilterParams,
+        },
+        {
+            headerName: "本日累積出荷数",
+            field: "shipNumber",
+        },
+        {
+            headerName: "キャパシティ",
+            children: [
+                {
+                    field: "linkCap",
+                    headerName: "連携キャップ",
+                    suppressMovable: true,
+                },
+                {
+                    field: "fixedCap",
+                    headerName: "確定キャップ",
+                    suppressMovable: true,
+                    editable: true,
+                    cellEditor: 'agNumberCellEditor',
+                    cellEditorParams: {
+                        min: 0,
+                        max: 100
+                    }
+                },
+            ]
+        },
+        {
+            headerName: "個別設定",
+            field: "indivConfig",
+            cellRenderer: (params: any) => {
+                if (params.node.group) return null;
+                return (
+                    <a
+                        href="#"
+                        onClick={(e) => {
+                            e.preventDefault(); // 阻止默认跳转
+                            handleConfigClick(params.data); // 传递当前行数据
+                        }}
+                    >
+                        設定
+                    </a>
+                );
+            },
+        }
+    ]);
+
+    const defaultColDef = useMemo<ColDef>(() => ({
+        flex: 1,
+        minWidth: 100,
+        floatingFilter: true,
+        suppressMenu: true,
+    }), []);
+
+    const autoGroupColumnDef = useMemo<ColDef>(() => ({
+        minWidth: 200,
+        filter: true,
+        floatingFilter: true,
+    }), []);
+
+    const gridRef = useRef<AgGridReact<IOlympicData>>(null);
+
+    // 加载状态和错误状态展示
+    if (loading) return <div style={{ padding: '20px' }}>データを読み込んでいます...</div>;
+    if (error) return <div style={{ padding: '20px', color: 'red' }}>エラー: {error}</div>;
+
+    return (
+        <div style={containerStyle}>
+            <div style={{ height: '600px', width: '90%' }}>
+                <AgGridReact<IOlympicData>
+                    ref={gridRef}
+                    rowData={rowData}
+                    columnDefs={columnDefs}
+                    defaultColDef={defaultColDef}
+                    groupDisplayType={"multipleColumns"}
+                    autoGroupColumnDef={autoGroupColumnDef}
+                    groupDefaultExpanded={1}
+                    // enableFilter={true}
+                    animateRows={true}
+                    suppressMenuHide={false}
+                    onCellValueChanged={onCellValueChanged}
+                />
+            </div>
+        </div>
+    );
+};
+
+export default GridExample;

+ 5 - 0
new-react-admin-ui/yarn.lock

@@ -1102,6 +1102,11 @@ ag-grid-react@^34.3.1:
     ag-grid-community "34.3.1"
     prop-types "^15.8.1"
 
+ag-grid@^18.1.2:
+  version "18.1.2"
+  resolved "https://registry.npmjs.org/ag-grid/-/ag-grid-18.1.2.tgz"
+  integrity sha512-HtJt8iFcRKCBj5UHBDmwSLLr72F3XDACeBNarH4nJWFHIqcnu7u0Ifrd2nftPmfEBj6YjFHawDqcZL2yo3YfmQ==
+
 ajv@^6.12.4:
   version "6.12.6"
   resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"

+ 1 - 10
ruoyi-admin/pom.xml

@@ -66,7 +66,7 @@
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
-                <version>2.5.15</version>
+                <version>3.3.0</version>
                 <configuration>
                     <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
                 </configuration>
@@ -78,15 +78,6 @@
                     </execution>
                 </executions>
             </plugin>
-            <plugin>   
-                <groupId>org.apache.maven.plugins</groupId>   
-                <artifactId>maven-war-plugin</artifactId>   
-                <version>3.1.0</version>   
-                <configuration>
-                    <failOnMissingWebXml>false</failOnMissingWebXml>
-                    <warName>${project.artifactId}</warName>
-                </configuration>   
-           </plugin>   
         </plugins>
         <finalName>${project.artifactId}</finalName>
     </build>

+ 3 - 0
ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java

@@ -3,6 +3,8 @@ package com.ruoyi;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import com.ruoyi.common.config.AppConfig;
 
 /**
  * 启动程序
@@ -10,6 +12,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
  * @author ruoyi
  */
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@EnableConfigurationProperties(AppConfig.class)
 public class RuoYiApplication
 {
     public static void main(String[] args)

+ 3 - 3
ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java

@@ -15,7 +15,7 @@ import com.ruoyi.common.config.RuoYiConfig;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.core.domain.AjaxResult;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.utils.sign.Base64;
 import com.ruoyi.common.utils.uuid.IdUtils;
 import com.ruoyi.system.service.ISysConfigService;
@@ -35,7 +35,7 @@ public class CaptchaController
     private Producer captchaProducerMath;
 
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
     
     @Autowired
     private ISysConfigService configService;
@@ -75,7 +75,7 @@ public class CaptchaController
             image = captchaProducer.createImage(capStr);
         }
 
-        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
+        appCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
         // 转换流信息写出
         FastByteArrayOutputStream os = new FastByteArrayOutputStream();
         try

+ 5 - 5
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java

@@ -17,7 +17,7 @@ import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.core.domain.model.LoginUser;
 import com.ruoyi.common.core.page.TableDataInfo;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.enums.BusinessType;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.system.domain.SysUserOnline;
@@ -36,17 +36,17 @@ public class SysUserOnlineController extends BaseController
     private ISysUserOnlineService userOnlineService;
 
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
 
     @PreAuthorize("@ss.hasPermi('monitor:online:list')")
     @GetMapping("/list")
     public TableDataInfo list(String ipaddr, String userName)
     {
-        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
+        Collection<String> keys = appCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
         List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
         for (String key : keys)
         {
-            LoginUser user = redisCache.getCacheObject(key);
+            LoginUser user = appCache.getCacheObject(key, LoginUser.class);
             if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
             {
                 userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
@@ -77,7 +77,7 @@ public class SysUserOnlineController extends BaseController
     @DeleteMapping("/{tokenId}")
     public AjaxResult forceLogout(@PathVariable String tokenId)
     {
-        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
+        appCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
         return success();
     }
 }

+ 24 - 22
ruoyi-admin/src/main/resources/application.yml

@@ -12,6 +12,8 @@ ruoyi:
   addressEnabled: false
   # 验证码类型 math 数字计算 char 字符验证
   captchaType: math
+  # 应用缓存配置 缓存类型: redis 或 db
+  cache: db
 
 # 开发环境配置
 server:
@@ -66,28 +68,28 @@ spring:
       # 热部署开关
       enabled: true
   data:
-    # redis 配置
-    redis:
-      # 地址
-      host: localhost
-      # 端口,默认为6379
-      port: 6379
-      # 数据库索引
-      database: 0
-      # 密码
-      password:
-      # 连接超时时间
-      timeout: 10s
-      lettuce:
-        pool:
-          # 连接池中的最小空闲连接
-          min-idle: 0
-          # 连接池中的最大空闲连接
-          max-idle: 8
-          # 连接池的最大数据库连接数
-          max-active: 8
-          # #连接池最大阻塞等待时间(使用负值表示没有限制)
-          max-wait: -1ms
+    # redis 配置 - 已禁用,使用数据库缓存
+    # redis:
+    #   # 地址
+    #   host: localhost
+    #   # 端口,默认为6379
+    #   port: 6379
+    #   # 数据库索引
+    #   database: 0
+    #   # 密码
+    #   password:
+    #   # 连接超时时间
+    #   timeout: 10s
+    #   lettuce:
+    #     pool:
+    #       # 连接池中的最小空闲连接
+    #       min-idle: 0
+    #       # 连接池中的最大空闲连接
+    #       max-idle: 8
+    #       # 连接池的最大数据库连接数
+    #       max-active: 8
+    #       # #连接池最大阻塞等待时间(使用负值表示没有限制)
+    #       max-wait: -1ms
 
 # token配置
 token:

+ 25 - 0
ruoyi-common/src/main/java/com/ruoyi/common/config/AppConfig.java

@@ -0,0 +1,25 @@
+package com.ruoyi.common.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 应用配置
+ * 
+ * @author ruoyi
+ */
+@Component
+@ConfigurationProperties(prefix = "ruoyi")
+public class AppConfig
+{
+    /** 缓存类型 */
+    private static String cache = "db";
+
+    public static String getCache() {
+        return cache;
+    }
+
+    public void setCache(String cache) {
+        AppConfig.cache = cache;
+    }
+}

+ 9 - 0
ruoyi-common/src/main/java/com/ruoyi/common/constant/CacheConstants.java

@@ -8,6 +8,15 @@ package com.ruoyi.common.constant;
 public class CacheConstants
 {
     /**
+     * 缓存工具类型 - Redis
+     */
+    public static final String CACHE_TOOL_REDIS = "redis";
+
+    /**
+     * 缓存工具类型 - 数据库
+     */
+    public static final String CACHE_TOOL_DB = "db";
+    /**
      * 登录用户 redis key
      */
     public static final String LOGIN_TOKEN_KEY = "login_tokens:";

+ 392 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/AppCache.java

@@ -0,0 +1,392 @@
+package com.ruoyi.common.core.cache;
+
+import com.ruoyi.common.config.AppConfig;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.utils.JacksonUtils;
+import com.ruoyi.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 应用缓存统一接口
+ * 根据配置选择使用Redis缓存或数据库缓存
+ * 
+ * @author ruoyi
+ */
+@Component
+public class AppCache {
+    
+    @Autowired
+    private RedisCache redisCache;
+    
+    @Autowired
+    private DbCache dbCache;
+    
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     */
+    public <T> void setCacheObject(final String key, final T value) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            dbCache.setCacheObject(key, value);
+        } else {
+            redisCache.setCacheObject(key, value);
+        }
+    }
+    
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     * @param timeout 时间
+     * @param timeUnit 时间颗粒度
+     */
+    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            dbCache.setCacheObject(key, value, timeout, timeUnit);
+        } else {
+            redisCache.setCacheObject(key, value, timeout, timeUnit);
+        }
+    }
+    
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.expire(key, timeout);
+        } else {
+            return redisCache.expire(key, timeout);
+        }
+    }
+    
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @param unit 时间单位
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.expire(key, timeout, unit);
+        } else {
+            return redisCache.expire(key, timeout, unit);
+        }
+    }
+    
+    /**
+     * 获得有效时间
+     *
+     * @param key Redis键
+     * @return 有效时间
+     */
+    public long getExpire(final String key) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.getExpire(key);
+        } else {
+            return redisCache.getExpire(key);
+        }
+    }
+    
+    /**
+     * 判断key是否存在
+     *
+     * @param key 键
+     * @return true:存在 / false:不存在
+     */
+    public Boolean hasKey(String key) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.hasKey(key);
+        } else {
+            return redisCache.hasKey(key);
+        }
+    }
+    
+    /**
+     * 获得缓存的基本对象。
+     *
+     * @param key 缓存键值
+     * @return 缓存键值对应的数据
+     */
+    public <T> T getCacheObject(final String key) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            Object result = dbCache.getCacheObject(key);
+            // 由于无法知道具体类型,返回Object类型,由调用方进行类型转换
+            return (T) result;
+        } else {
+            return redisCache.getCacheObject(key);
+        }
+    }
+    
+    /**
+     * 获得缓存的基本对象。
+     *
+     * @param key 缓存键值
+     * @param clazz 对象类型
+     * @return 缓存键值对应的数据
+     */
+    public <T> T getCacheObject(final String key, Class<T> clazz) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            // 直接调用DbCache的类型安全方法
+            return dbCache.getCacheObject(key, clazz);
+        } else {
+            return redisCache.getCacheObject(key);
+        }
+    }
+    
+    /**
+     * 获得缓存的基本对象(支持泛型类型)
+     *
+     * @param key 缓存键值
+     * @param typeReference 类型引用
+     * @return 缓存键值对应的数据
+     */
+    public <T> T getCacheObject(final String key, com.fasterxml.jackson.core.type.TypeReference<T> typeReference) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            // 直接调用DbCache的类型安全方法
+            return dbCache.getCacheObject(key, typeReference);
+        } else {
+            // Redis缓存不支持TypeReference,返回Object类型
+            Object result = redisCache.getCacheObject(key);
+            return (T) result;
+        }
+    }
+    
+    /**
+     * 删除单个对象
+     *
+     * @param key
+     */
+    public boolean deleteObject(final String key) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.deleteObject(key);
+        } else {
+            return redisCache.deleteObject(key);
+        }
+    }
+    
+    /**
+     * 删除集合对象
+     *
+     * @param collection 多个对象
+     * @return
+     */
+    public boolean deleteObject(final Collection collection) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.deleteObject(collection);
+        } else {
+            return redisCache.deleteObject(collection);
+        }
+    }
+    
+    /**
+     * 缓存List数据
+     *
+     * @param key 缓存的键值
+     * @param dataList 待缓存的List数据
+     * @return 缓存的对象
+     */
+    public <T> long setCacheList(final String key, final List<T> dataList) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.setCacheList(key, dataList);
+        } else {
+            return redisCache.setCacheList(key, dataList);
+        }
+    }
+    
+    /**
+     * 获得缓存的list对象
+     *
+     * @param key 缓存的键值
+     * @return 缓存键值对应的数据
+     */
+    public <T> List<T> getCacheList(final String key) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            Object result = dbCache.getCacheList(key);
+            // 由于DbCache.getCacheList返回的是泛型List类型,直接强制转换
+            return (List<T>) result;
+        } else {
+            return redisCache.getCacheList(key);
+        }
+    }
+    
+    /**
+     * 缓存Set
+     *
+     * @param key 缓存键值
+     * @param dataSet 缓存的数据
+     * @return 缓存数据的对象
+     */
+    public <T> long setCacheSet(final String key, final Set<T> dataSet) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.setCacheSet(key, dataSet);
+        } else {
+            // RedisCache.setCacheSet返回BoundSetOperations,但我们需要返回long类型
+            // 这里我们调用setCacheSet但不使用返回值,返回一个默认值
+            redisCache.setCacheSet(key, dataSet);
+            return dataSet.size();
+        }
+    }
+    
+    /**
+     * 获得缓存的set
+     *
+     * @param key
+     * @return
+     */
+    public <T> Set<T> getCacheSet(final String key) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            Object result = dbCache.getCacheSet(key);
+            // 由于DbCache.getCacheSet返回的是泛型Set类型,直接强制转换
+            return (Set<T>) result;
+        } else {
+            return redisCache.getCacheSet(key);
+        }
+    }
+    
+    /**
+     * 缓存Map
+     *
+     * @param key
+     * @param dataMap
+     */
+    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            dbCache.setCacheMap(key, dataMap);
+        } else {
+            redisCache.setCacheMap(key, dataMap);
+        }
+    }
+    
+    /**
+     * 获得缓存的Map
+     *
+     * @param key
+     * @return
+     */
+    public <T> Map<String, T> getCacheMap(final String key) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            Object result = dbCache.getCacheMap(key);
+            // 由于DbCache.getCacheMap返回的是泛型Map类型,直接强制转换
+            return (Map<String, T>) result;
+        } else {
+            return redisCache.getCacheMap(key);
+        }
+    }
+    
+    /**
+     * 往Hash中存入数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @param value 值
+     */
+    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            dbCache.setCacheMapValue(key, hKey, value);
+        } else {
+            redisCache.setCacheMapValue(key, hKey, value);
+        }
+    }
+    
+    /**
+     * 获取Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @return Hash中的对象
+     */
+    public <T> T getCacheMapValue(final String key, final String hKey) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            Object result = dbCache.getCacheMapValue(key, hKey);
+            if (result instanceof String) {
+                // 数据库缓存返回的是JSON字符串,需要解析
+                // 由于无法知道具体类型,返回Object类型,由调用方进行类型转换
+                return (T) result;
+            }
+            return (T) result;
+        } else {
+            return redisCache.getCacheMapValue(key, hKey);
+        }
+    }
+    
+    /**
+     * 删除Hash中的数据
+     * 
+     * @param key
+     * @param hkey
+     */
+    public void delCacheMapValue(final String key, final String hkey) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            dbCache.delCacheMapValue(key, hkey);
+        } else {
+            redisCache.deleteCacheMapValue(key, hkey);
+        }
+    }
+    
+    /**
+     * 获取多个Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKeys Hash键集合
+     * @return Hash对象集合
+     */
+    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.getMultiCacheMapValue(key, hKeys);
+        } else {
+            return redisCache.getMultiCacheMapValue(key, hKeys);
+        }
+    }
+    
+    /**
+     * 获得缓存的基本对象列表
+     * 
+     * @param pattern 字符串前缀
+     * @return 对象列表
+     */
+    public Collection<String> keys(final String pattern) {
+        // 数据库缓存
+        if(StringUtils.equals(AppConfig.getCache(), CacheConstants.CACHE_TOOL_DB) ){
+            return dbCache.keys(pattern);
+        } else {
+            return redisCache.keys(pattern);
+        }
+    }
+}

+ 491 - 0
ruoyi-common/src/main/java/com/ruoyi/common/core/cache/DbCache.java

@@ -0,0 +1,491 @@
+package com.ruoyi.common.core.cache;
+
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.JacksonUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 数据库缓存实现
+ * 使用sys_cache表存储缓存数据
+ * 
+ * @author ruoyi
+ */
+@Component
+public class DbCache {
+    
+    private static final Logger log = LoggerFactory.getLogger(DbCache.class);
+    
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+    
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     */
+    public <T> void setCacheObject(final String key, final T value) {
+        setCacheObject(key, value, null, null);
+    }
+    
+    /**
+     * 缓存基本的对象,Integer、String、实体类等
+     *
+     * @param key 缓存的键值
+     * @param value 缓存的值
+     * @param timeout 时间
+     * @param timeUnit 时间颗粒度
+     */
+    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
+        if (StringUtils.isAnyBlank(key) || value == null) {
+            return;
+        }
+        
+        try {
+            String cacheValue = JacksonUtils.toJSONString(value);
+            Timestamp expiryTime = calculateExpiryTime(timeout, timeUnit);
+            
+            // 使用INSERT ... ON CONFLICT语句实现插入或更新(PostgreSQL兼容)
+            String sql = "INSERT INTO sys_cache (cache_key, cache_value, expiry_time, create_time, update_time) " +
+                        "VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) " +
+                        "ON CONFLICT (cache_key) DO UPDATE SET " +
+                        "cache_value = EXCLUDED.cache_value, expiry_time = EXCLUDED.expiry_time, update_time = CURRENT_TIMESTAMP";
+            
+            jdbcTemplate.update(sql, key, cacheValue, expiryTime);
+        } catch (Exception e) {
+            log.error("设置缓存失败 key: {}", key, e);
+        }
+    }
+    
+    /**
+     * 设置有效时间(默认时间单位为秒)
+     *
+     * @param key Redis键
+     * @param timeout 超时时间(秒)
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout) {
+        return expire(key, timeout, TimeUnit.SECONDS);
+    }
+    
+    /**
+     * 设置有效时间
+     *
+     * @param key Redis键
+     * @param timeout 超时时间
+     * @param unit 时间单位
+     * @return true=设置成功;false=设置失败
+     */
+    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
+        if (StringUtils.isBlank(key)) {
+            return false;
+        }
+        
+        try {
+            Timestamp expiryTime = calculateExpiryTime((int) timeout, unit);
+            String sql = "UPDATE sys_cache SET expiry_time = ?, update_time = CURRENT_TIMESTAMP WHERE cache_key = ?";
+            int affected = jdbcTemplate.update(sql, expiryTime, key);
+            return affected > 0;
+        } catch (Exception e) {
+            log.error("设置缓存过期时间失败 key: {}", key, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 获得缓存的基本对象。
+     *
+     * @param key 缓存键值
+     * @return 缓存键值对应的数据
+     */
+    public Object getCacheObject(final String key) {
+        if (StringUtils.isBlank(key)) {
+            return null;
+        }
+        
+        try {
+            // 先清理过期缓存
+            cleanupExpiredCache();
+            
+            String sql = "SELECT cache_value FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
+            String cacheValue = jdbcTemplate.queryForObject(sql, String.class, key);
+            
+            if (StringUtils.isNotBlank(cacheValue)) {
+                // 由于数据库缓存存储的是JSON字符串,我们无法知道具体类型
+                // 返回Object类型,由调用方进行类型转换
+                return cacheValue;
+            }
+        } catch (EmptyResultDataAccessException e) {
+            // 缓存不存在,返回null
+        } catch (Exception e) {
+            log.error("获取缓存失败 key: {}", key, e);
+        }
+        
+        return null;
+    }
+    
+    /**
+     * 获得缓存的基本对象(类型安全版本)
+     *
+     * @param key 缓存键值
+     * @param clazz 对象类型
+     * @return 缓存键值对应的数据
+     */
+    public <T> T getCacheObject(final String key, Class<T> clazz) {
+        if (StringUtils.isBlank(key)) {
+            return null;
+        }
+        
+        try {
+            // 先清理过期缓存
+            cleanupExpiredCache();
+            
+            String sql = "SELECT cache_value FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
+            String cacheValue = jdbcTemplate.queryForObject(sql, String.class, key);
+            
+            if (StringUtils.isNotBlank(cacheValue)) {
+                // 解析JSON字符串为指定类型
+                return JacksonUtils.parseObject(cacheValue, clazz);
+            }
+        } catch (EmptyResultDataAccessException e) {
+            // 缓存不存在,返回null
+        } catch (Exception e) {
+            log.error("获取缓存失败 key: {}", key, e);
+        }
+        
+        return null;
+    }
+    
+    /**
+     * 获得缓存的基本对象(支持泛型类型)
+     *
+     * @param key 缓存键值
+     * @param typeReference 类型引用
+     * @return 缓存键值对应的数据
+     */
+    public <T> T getCacheObject(final String key, com.fasterxml.jackson.core.type.TypeReference<T> typeReference) {
+        if (StringUtils.isBlank(key)) {
+            return null;
+        }
+        
+        try {
+            // 先清理过期缓存
+            cleanupExpiredCache();
+            
+            String sql = "SELECT cache_value FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
+            String cacheValue = jdbcTemplate.queryForObject(sql, String.class, key);
+            
+            if (StringUtils.isNotBlank(cacheValue)) {
+                // 解析JSON字符串为指定类型
+                return JacksonUtils.parseObject(cacheValue, typeReference);
+            }
+        } catch (EmptyResultDataAccessException e) {
+            // 缓存不存在,返回null
+        } catch (Exception e) {
+            log.error("获取缓存失败 key: {}", key, e);
+        }
+        
+        return null;
+    }
+    
+    /**
+     * 删除单个对象
+     *
+     * @param key
+     */
+    public boolean deleteObject(final String key) {
+        if (StringUtils.isBlank(key)) {
+            return false;
+        }
+        
+        try {
+            String sql = "DELETE FROM sys_cache WHERE cache_key = ?";
+            int affected = jdbcTemplate.update(sql, key);
+            return affected > 0;
+        } catch (Exception e) {
+            log.error("删除缓存失败 key: {}", key, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 删除集合对象
+     *
+     * @param collection 多个对象
+     * @return
+     */
+    public boolean deleteObject(final Collection collection) {
+        if (collection == null || collection.isEmpty()) {
+            return false;
+        }
+        
+        try {
+            String sql = "DELETE FROM sys_cache WHERE cache_key IN (" + 
+                        StringUtils.join(collection, ",") + ")";
+            int affected = jdbcTemplate.update(sql);
+            return affected > 0;
+        } catch (Exception e) {
+            log.error("批量删除缓存失败", e);
+            return false;
+        }
+    }
+    
+    /**
+     * 缓存List数据
+     *
+     * @param key 缓存的键值
+     * @param dataList 待缓存的List数据
+     * @return 缓存的对象
+     */
+    public <T> long setCacheList(final String key, final List<T> dataList) {
+        setCacheObject(key, dataList);
+        return dataList != null ? dataList.size() : 0;
+    }
+    
+    /**
+     * 获得缓存的list对象
+     *
+     * @param key 缓存的键值
+     * @return 缓存键值对应的数据
+     */
+    public <T> List<T> getCacheList(final String key) {
+        Object result = getCacheObject(key);
+        if (result instanceof String) {
+            // 解析JSON字符串为List
+            return JacksonUtils.parseObject((String) result, List.class);
+        }
+        return (List<T>) result;
+    }
+    
+    /**
+     * 缓存Set
+     *
+     * @param key 缓存键值
+     * @param dataSet 缓存的数据
+     * @return 缓存数据的对象
+     */
+    public <T> long setCacheSet(final String key, final Set<T> dataSet) {
+        setCacheObject(key, dataSet);
+        return dataSet != null ? dataSet.size() : 0;
+    }
+    
+    /**
+     * 获得缓存的set
+     *
+     * @param key
+     * @return
+     */
+    public <T> Set<T> getCacheSet(final String key) {
+        Object result = getCacheObject(key);
+        if (result instanceof String) {
+            // 解析JSON字符串为Set
+            return JacksonUtils.parseObject((String) result, Set.class);
+        }
+        return (Set<T>) result;
+    }
+    
+    /**
+     * 缓存Map
+     *
+     * @param key
+     * @param dataMap
+     */
+    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
+        setCacheObject(key, dataMap);
+    }
+    
+    /**
+     * 获得缓存的Map
+     *
+     * @param key
+     * @return
+     */
+    public <T> Map<String, T> getCacheMap(final String key) {
+        Object result = getCacheObject(key);
+        if (result instanceof String) {
+            // 解析JSON字符串为Map
+            return JacksonUtils.parseObject((String) result, Map.class);
+        }
+        return (Map<String, T>) result;
+    }
+    
+    /**
+     * 往Hash中存入数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @param value 值
+     */
+    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
+        // 数据库缓存不支持Hash结构,使用普通缓存
+        setCacheObject(key + ":" + hKey, value);
+    }
+    
+    /**
+     * 获取Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKey Hash键
+     * @return Hash中的对象
+     */
+    public <T> T getCacheMapValue(final String key, final String hKey) {
+        Object result = getCacheObject(key + ":" + hKey);
+        if (result instanceof String) {
+            // 由于数据库缓存存储的是JSON字符串,我们无法知道具体类型
+            // 返回Object类型,由调用方进行类型转换
+            return (T) result;
+        }
+        return (T) result;
+    }
+    
+    /**
+     * 删除Hash中的数据
+     * 
+     * @param key
+     * @param hkey
+     */
+    public void delCacheMapValue(final String key, final String hkey) {
+        deleteObject(key + ":" + hkey);
+    }
+    
+    /**
+     * 获取多个Hash中的数据
+     *
+     * @param key Redis键
+     * @param hKeys Hash键集合
+     * @return Hash对象集合
+     */
+    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
+        List<T> result = new ArrayList<>();
+        for (Object hKey : hKeys) {
+            T value = getCacheMapValue(key, hKey.toString());
+            if (value != null) {
+                result.add(value);
+            }
+        }
+        return result;
+    }
+    
+    /**
+     * 获得缓存的基本对象列表
+     * 
+     * @param pattern 字符串前缀
+     * @return 对象列表
+     */
+    public Collection<String> keys(final String pattern) {
+        try {
+            // 清理过期缓存
+            cleanupExpiredCache();
+            
+            String sql = "SELECT cache_key FROM sys_cache WHERE cache_key LIKE ? AND expiry_time > CURRENT_TIMESTAMP";
+            return jdbcTemplate.queryForList(sql, String.class, pattern.replace("*", "%"));
+        } catch (Exception e) {
+            log.error("查询缓存键失败 pattern: {}", pattern, e);
+            return Collections.emptyList();
+        }
+    }
+    
+    /**
+     * 获取缓存的有效期
+     *
+     * @param key Redis键
+     * @return 有效期(秒)
+     */
+    public long getExpire(final String key) {
+        if (StringUtils.isBlank(key)) {
+            return -1;
+        }
+        
+        try {
+            String sql = "SELECT expiry_time FROM sys_cache WHERE cache_key = ?";
+            Timestamp expiryTime = jdbcTemplate.queryForObject(sql, Timestamp.class, key);
+            
+            if (expiryTime != null) {
+                long remaining = expiryTime.getTime() - System.currentTimeMillis();
+                return remaining > 0 ? remaining / 1000 : -1;
+            }
+        } catch (EmptyResultDataAccessException e) {
+            // 缓存不存在
+        } catch (Exception e) {
+            log.error("获取缓存有效期失败 key: {}", key, e);
+        }
+        
+        return -1;
+    }
+    
+    /**
+     * 判断key是否存在
+     *
+     * @param key 键
+     * @return true 存在 false不存在
+     */
+    public boolean hasKey(final String key) {
+        if (StringUtils.isBlank(key)) {
+            return false;
+        }
+        
+        try {
+            // 清理过期缓存
+            cleanupExpiredCache();
+            
+            String sql = "SELECT COUNT(*) FROM sys_cache WHERE cache_key = ? AND expiry_time > CURRENT_TIMESTAMP";
+            Integer count = jdbcTemplate.queryForObject(sql, Integer.class, key);
+            return count != null && count > 0;
+        } catch (Exception e) {
+            log.error("检查缓存存在性失败 key: {}", key, e);
+            return false;
+        }
+    }
+    
+    /**
+     * 清理过期缓存
+     */
+    private void cleanupExpiredCache() {
+        try {
+            String sql = "DELETE FROM sys_cache WHERE expiry_time <= CURRENT_TIMESTAMP";
+            jdbcTemplate.update(sql);
+        } catch (Exception e) {
+            log.error("清理过期缓存失败", e);
+        }
+    }
+    
+    /**
+     * 计算过期时间
+     */
+    private Timestamp calculateExpiryTime(Integer timeout, TimeUnit timeUnit) {
+        if (timeout == null || timeUnit == null) {
+            // 默认30天过期
+            return Timestamp.valueOf(LocalDateTime.now().plusDays(30));
+        }
+        
+        LocalDateTime expiryTime = LocalDateTime.now();
+        switch (timeUnit) {
+            case SECONDS:
+                expiryTime = expiryTime.plusSeconds(timeout);
+                break;
+            case MINUTES:
+                expiryTime = expiryTime.plusMinutes(timeout);
+                break;
+            case HOURS:
+                expiryTime = expiryTime.plusHours(timeout);
+                break;
+            case DAYS:
+                expiryTime = expiryTime.plusDays(timeout);
+                break;
+            default:
+                expiryTime = expiryTime.plusDays(30);
+        }
+        
+        return Timestamp.valueOf(expiryTime);
+    }
+}

+ 1 - 1
ruoyi-common/src/main/java/com/ruoyi/common/core/redis/RedisCache.java

@@ -1,4 +1,4 @@
-package com.ruoyi.common.core.redis;
+package com.ruoyi.common.core.cache;
 
 import java.util.Collection;
 import java.util.Iterator;

+ 6 - 6
ruoyi-common/src/main/java/com/ruoyi/common/utils/DictUtils.java

@@ -4,7 +4,7 @@ import java.util.Collection;
 import java.util.List;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.core.domain.entity.SysDictData;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.utils.spring.SpringUtils;
 
 /**
@@ -27,7 +27,7 @@ public class DictUtils
      */
     public static void setDictCache(String key, List<SysDictData> dictDatas)
     {
-        SpringUtils.getBean(RedisCache.class).setCacheObject(getCacheKey(key), dictDatas);
+        SpringUtils.getBean(AppCache.class).setCacheObject(getCacheKey(key), dictDatas);
     }
 
     /**
@@ -38,7 +38,7 @@ public class DictUtils
      */
     public static List<SysDictData> getDictCache(String key)
     {
-        List<SysDictData> dictCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
+        List<SysDictData> dictCache = SpringUtils.getBean(AppCache.class).getCacheObject(getCacheKey(key), new com.fasterxml.jackson.core.type.TypeReference<List<SysDictData>>() {});
         if (StringUtils.isNotNull(dictCache))
         {
             return dictCache;
@@ -213,7 +213,7 @@ public class DictUtils
      */
     public static void removeDictCache(String key)
     {
-        SpringUtils.getBean(RedisCache.class).deleteObject(getCacheKey(key));
+        SpringUtils.getBean(AppCache.class).deleteObject(getCacheKey(key));
     }
 
     /**
@@ -221,8 +221,8 @@ public class DictUtils
      */
     public static void clearDictCache()
     {
-        Collection<String> keys = SpringUtils.getBean(RedisCache.class).keys(CacheConstants.SYS_DICT_KEY + "*");
-        SpringUtils.getBean(RedisCache.class).deleteObject(keys);
+        Collection<String> keys = SpringUtils.getBean(AppCache.class).keys(CacheConstants.SYS_DICT_KEY + "*");
+        SpringUtils.getBean(AppCache.class).deleteObject(keys);
     }
 
     /**

+ 15 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/JacksonUtils.java

@@ -83,4 +83,19 @@ public class JacksonUtils
             throw new RuntimeException("JSON反序列化失败", e);
         }
     }
+    
+    /**
+     * JSON字符串转对象(支持泛型类型)
+     */
+    public static <T> T parseObject(String json, com.fasterxml.jackson.core.type.TypeReference<T> typeReference)
+    {
+        try
+        {
+            return objectMapper.readValue(json, typeReference);
+        }
+        catch (JsonProcessingException e)
+        {
+            throw new RuntimeException("JSON反序列化失败", e);
+        }
+    }
 }

+ 5 - 6
ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java

@@ -10,7 +10,7 @@ import org.springframework.stereotype.Component;
 import com.ruoyi.common.utils.JacksonUtils;
 import com.ruoyi.common.annotation.RepeatSubmit;
 import com.ruoyi.common.constant.CacheConstants;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.filter.RepeatedlyRequestWrapper;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.http.HttpHelper;
@@ -34,7 +34,7 @@ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
     private String header;
 
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
 
     @SuppressWarnings("unchecked")
     @Override
@@ -65,10 +65,9 @@ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
         // 唯一标识(指定key + url + 消息头)
         String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
 
-        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
-        if (sessionObj != null)
+        Map<String, Object> sessionMap = appCache.getCacheObject(cacheRepeatKey, new com.fasterxml.jackson.core.type.TypeReference<Map<String, Object>>() {});
+        if (sessionMap != null)
         {
-            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
             if (sessionMap.containsKey(url))
             {
                 Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
@@ -80,7 +79,7 @@ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
         }
         Map<String, Object> cacheMap = new HashMap<String, Object>();
         cacheMap.put(url, nowDataMap);
-        redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
+        appCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
         return false;
     }
 

+ 4 - 4
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java

@@ -12,7 +12,7 @@ import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.core.domain.model.LoginUser;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.exception.user.BlackListException;
 import com.ruoyi.common.exception.user.CaptchaException;
@@ -44,7 +44,7 @@ public class SysLoginService
     private AuthenticationManager authenticationManager;
 
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
     
     @Autowired
     private ISysUserService userService;
@@ -112,13 +112,13 @@ public class SysLoginService
         if (captchaEnabled)
         {
             String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
-            String captcha = redisCache.getCacheObject(verifyKey);
+            String captcha = appCache.getCacheObject(verifyKey);
             if (captcha == null)
             {
                 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
                 throw new CaptchaExpireException();
             }
-            redisCache.deleteObject(verifyKey);
+            appCache.deleteObject(verifyKey);
             if (!code.equalsIgnoreCase(captcha))
             {
                 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));

+ 6 - 6
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java

@@ -7,7 +7,7 @@ import org.springframework.security.core.Authentication;
 import org.springframework.stereotype.Component;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.core.domain.entity.SysUser;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
 import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
 import com.ruoyi.common.utils.SecurityUtils;
@@ -22,7 +22,7 @@ import com.ruoyi.framework.security.context.AuthenticationContextHolder;
 public class SysPasswordService
 {
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
 
     @Value(value = "${user.password.maxRetryCount}")
     private int maxRetryCount;
@@ -47,7 +47,7 @@ public class SysPasswordService
         String username = usernamePasswordAuthenticationToken.getName();
         String password = usernamePasswordAuthenticationToken.getCredentials().toString();
 
-        Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
+        Integer retryCount = appCache.getCacheObject(getCacheKey(username), Integer.class);
 
         if (retryCount == null)
         {
@@ -62,7 +62,7 @@ public class SysPasswordService
         if (!matches(user, password))
         {
             retryCount = retryCount + 1;
-            redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
+            appCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
             throw new UserPasswordNotMatchException();
         }
         else
@@ -78,9 +78,9 @@ public class SysPasswordService
 
     public void clearLoginRecordCache(String loginName)
     {
-        if (redisCache.hasKey(getCacheKey(loginName)))
+        if (appCache.hasKey(getCacheKey(loginName)))
         {
-            redisCache.deleteObject(getCacheKey(loginName));
+            appCache.deleteObject(getCacheKey(loginName));
         }
     }
 }

+ 4 - 4
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java

@@ -7,7 +7,7 @@ import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.core.domain.model.RegisterBody;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.exception.user.CaptchaException;
 import com.ruoyi.common.exception.user.CaptchaExpireException;
 import com.ruoyi.common.utils.MessageUtils;
@@ -33,7 +33,7 @@ public class SysRegisterService
     private ISysConfigService configService;
 
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
 
     /**
      * 注册
@@ -101,8 +101,8 @@ public class SysRegisterService
     public void validateCaptcha(String username, String code, String uuid)
     {
         String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
-        String captcha = redisCache.getCacheObject(verifyKey);
-        redisCache.deleteObject(verifyKey);
+        String captcha = appCache.getCacheObject(verifyKey);
+        appCache.deleteObject(verifyKey);
         if (captcha == null)
         {
             throw new CaptchaExpireException();

+ 5 - 5
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java

@@ -12,7 +12,7 @@ import org.springframework.stereotype.Component;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.core.domain.model.LoginUser;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.utils.ServletUtils;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.ip.AddressUtils;
@@ -52,7 +52,7 @@ public class TokenService
     private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
 
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
 
     /**
      * 获取用户身份信息
@@ -71,7 +71,7 @@ public class TokenService
                 // 解析对应的权限以及用户信息
                 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                 String userKey = getTokenKey(uuid);
-                LoginUser user = redisCache.getCacheObject(userKey);
+                LoginUser user = appCache.getCacheObject(userKey, LoginUser.class);
                 return user;
             }
             catch (Exception e)
@@ -101,7 +101,7 @@ public class TokenService
         if (StringUtils.isNotEmpty(token))
         {
             String userKey = getTokenKey(token);
-            redisCache.deleteObject(userKey);
+            appCache.deleteObject(userKey);
         }
     }
 
@@ -150,7 +150,7 @@ public class TokenService
         loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
         // 根据uuid将loginUser缓存
         String userKey = getTokenKey(loginUser.getToken());
-        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
+        appCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
     }
 
     /**

+ 19 - 13
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysConfigServiceImpl.java

@@ -8,7 +8,7 @@ import org.springframework.stereotype.Service;
 import com.ruoyi.common.annotation.DataSource;
 import com.ruoyi.common.constant.CacheConstants;
 import com.ruoyi.common.constant.UserConstants;
-import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.core.cache.AppCache;
 import com.ruoyi.common.core.text.Convert;
 import com.ruoyi.common.enums.DataSourceType;
 import com.ruoyi.common.exception.ServiceException;
@@ -29,7 +29,7 @@ public class SysConfigServiceImpl implements ISysConfigService
     private SysConfigMapper configMapper;
 
     @Autowired
-    private RedisCache redisCache;
+    private AppCache appCache;
 
     /**
      * 项目启动时,初始化参数到缓存
@@ -37,7 +37,12 @@ public class SysConfigServiceImpl implements ISysConfigService
     @PostConstruct
     public void init()
     {
-        loadingConfigCache();
+        try {
+            loadingConfigCache();
+        } catch (Exception e) {
+            // 启动时缓存初始化失败,记录日志但不阻止应用启动
+            System.err.println("缓存初始化失败,应用将继续启动: " + e.getMessage());
+        }
     }
 
     /**
@@ -64,7 +69,7 @@ public class SysConfigServiceImpl implements ISysConfigService
     @Override
     public String selectConfigByKey(String configKey)
     {
-        String configValue = Convert.toStr(redisCache.getCacheObject(getCacheKey(configKey)));
+        String configValue = Convert.toStr(appCache.getCacheObject(getCacheKey(configKey)));
         if (StringUtils.isNotEmpty(configValue))
         {
             return configValue;
@@ -74,7 +79,7 @@ public class SysConfigServiceImpl implements ISysConfigService
         SysConfig retConfig = configMapper.selectConfig(config);
         if (StringUtils.isNotNull(retConfig))
         {
-            redisCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());
+            appCache.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());
             return retConfig.getConfigValue();
         }
         return StringUtils.EMPTY;
@@ -93,7 +98,8 @@ public class SysConfigServiceImpl implements ISysConfigService
         {
             return true;
         }
-        return Convert.toBool(captchaEnabled);
+        Boolean result = Convert.toBool(captchaEnabled);
+        return result != null ? result : true;
     }
 
     /**
@@ -120,7 +126,7 @@ public class SysConfigServiceImpl implements ISysConfigService
         int row = configMapper.insertConfig(config);
         if (row > 0)
         {
-            redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
+            appCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
         }
         return row;
     }
@@ -137,13 +143,13 @@ public class SysConfigServiceImpl implements ISysConfigService
         SysConfig temp = configMapper.selectConfigById(config.getConfigId());
         if (!StringUtils.equals(temp.getConfigKey(), config.getConfigKey()))
         {
-            redisCache.deleteObject(getCacheKey(temp.getConfigKey()));
+            appCache.deleteObject(getCacheKey(temp.getConfigKey()));
         }
 
         int row = configMapper.updateConfig(config);
         if (row > 0)
         {
-            redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
+            appCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
         }
         return row;
     }
@@ -164,7 +170,7 @@ public class SysConfigServiceImpl implements ISysConfigService
                 throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey()));
             }
             configMapper.deleteConfigById(configId);
-            redisCache.deleteObject(getCacheKey(config.getConfigKey()));
+            appCache.deleteObject(getCacheKey(config.getConfigKey()));
         }
     }
 
@@ -177,7 +183,7 @@ public class SysConfigServiceImpl implements ISysConfigService
         List<SysConfig> configsList = configMapper.selectConfigList(new SysConfig());
         for (SysConfig config : configsList)
         {
-            redisCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
+            appCache.setCacheObject(getCacheKey(config.getConfigKey()), config.getConfigValue());
         }
     }
 
@@ -187,8 +193,8 @@ public class SysConfigServiceImpl implements ISysConfigService
     @Override
     public void clearConfigCache()
     {
-        Collection<String> keys = redisCache.keys(CacheConstants.SYS_CONFIG_KEY + "*");
-        redisCache.deleteObject(keys);
+        Collection<String> keys = appCache.keys(CacheConstants.SYS_CONFIG_KEY + "*");
+        appCache.deleteObject(keys);
     }
 
     /**

+ 67 - 0
ruoyi-system/src/main/resources/mapper/system/SysOrderMapper.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysOrderMapper">
+
+    <resultMap type="SysOrder" id="SysOrderMapper">
+        <id     property="depName"             column="dep_name"               />
+        <result property="regName"           column="reg_name"             />
+        <result property="baseCode"            column="base_code"              />
+        <result property="baseName"           column="base_name"             />
+        <result property="shipNumber"          column="ship_number"            />
+        <result property="linkCap"          column="link_cap"   />
+        <result property="fixedCap"  column="fixed_cap"   />
+        <result property="sorterNum"  column="sorter_num"   />
+        <result property="rejectNum"  column="reject_num"   />
+        <result property="sbsNum"  column="sbs_num"   />
+        <result property="dateTm"  column="date_tm"   />
+        <result property="updateNm"  column="update_nm"   />
+    </resultMap>
+
+    <sql id="selectOrderVo">
+        select r.dep_name,r.reg_name,r.base_code,r.base_name,r.ship_number,r.link_cap,r.fixed_cap
+        from hn_dash_board r
+    </sql>
+
+    <select id="selectOrderAll" resultMap="SysOrderMapper">
+        <include refid="selectOrderVo"/>
+    </select>
+
+    <select id="selectOrderDate" parameterType="SysOrder" resultMap="SysOrderMapper" >
+        select r.dep_name,r.reg_name,r.base_code,r.base_name,r.ship_number,r.link_cap,r.fixed_cap,rm.sorter_num,rm.reject_num,rm.sbs_num,rm.result_num,rm.date_tm,rm.update_nm
+        from hn_dash_board r
+        left join hn_date_base rm on r.dep_name = rm.dep_name and r.reg_name = rm.reg_name and r.base_code = rm.base_code and r.base_name = rm.base_name
+        <where>
+            1=1
+            <if test="depName != null and depName != ''"  >
+                And  r.dep_name = #{depName}
+            </if>
+
+            <if test="regName != null and regName != ''"  >
+                And  r.reg_name = #{regName}
+            </if>
+
+            <if test="baseCode != null and baseCode != ''"  >
+                And  r.base_code = #{baseCode}
+            </if>
+
+            <if test="baseName != null and baseName != ''"  >
+                And  r.base_name = #{baseName}
+            </if>
+        </where>
+
+    </select>
+
+
+    <update id="updatedashBoard" parameterType="SysOrder">
+        update hn_dash_board
+        <set>
+            <if test="fixedCap != null and fixedCap != ''">fixed_cap = #{fixedCap}, </if>
+        </set>
+        where dep_name = #{depName}
+        And reg_name = #{regName}
+        And base_code = #{baseCode}
+        And base_name = #{baseName}
+    </update>
+</mapper>

+ 32 - 0
sql/sys_cache.sql

@@ -0,0 +1,32 @@
+-- ----------------------------
+-- 系统缓存表
+-- 用于存储缓存数据,替代Redis功能
+-- ----------------------------
+DROP TABLE IF EXISTS sys_cache CASCADE;
+CREATE TABLE sys_cache (
+    cache_key     VARCHAR(255) NOT NULL PRIMARY KEY,
+    cache_value   TEXT         NOT NULL,
+    expiry_time   TIMESTAMP(0) NOT NULL,
+    create_time   TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP,
+    update_time   TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 创建索引以提高查询性能
+CREATE INDEX idx_sys_cache_expiry_time ON sys_cache(expiry_time);
+CREATE INDEX idx_sys_cache_create_time ON sys_cache(create_time);
+
+-- 添加注释
+COMMENT ON TABLE sys_cache IS '系统缓存表';
+COMMENT ON COLUMN sys_cache.cache_key IS '缓存键';
+COMMENT ON COLUMN sys_cache.cache_value IS '缓存值';
+COMMENT ON COLUMN sys_cache.expiry_time IS '过期时间';
+COMMENT ON COLUMN sys_cache.create_time IS '创建时间';
+COMMENT ON COLUMN sys_cache.update_time IS '更新时间';
+
+-- 创建定时清理过期缓存数据的函数(可选)
+CREATE OR REPLACE FUNCTION cleanup_expired_cache()
+RETURNS VOID AS $$
+BEGIN
+    DELETE FROM sys_cache WHERE expiry_time < CURRENT_TIMESTAMP;
+END;
+$$ LANGUAGE plpgsql;