From 1194038279d8a378f2ce7cbea59a32d753becbf8 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 08:16:47 +0800
Subject: [PATCH] feat: update rsf-design and redis integration

---
 rsf-design/tests/system-role-scope-contract.test.mjs                                 |   21 +
 rsf-design/src/api/system-manage.js                                                  |  135 ++++++++
 rsf-design/src/utils/router.js                                                       |    9 
 rsf-design/tests/system-manage-contract.test.mjs                                     |   20 +
 rsf-server/src/test/java/com/vincent/rsf/server/common/service/RedisServiceTest.java |   53 +++
 rsf-design/.env.development                                                          |    2 
 rsf-design/tests/system-user-page-contract.test.mjs                                  |   10 
 rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java     |  252 +++++------------
 rsf-design/src/views/system/menu/index.vue                                           |   29 +
 rsf-design/src/locales/langs/en.json                                                 |  109 +++++++
 rsf-design/src/locales/langs/zh.json                                                 |  111 +++++++
 rsf-design/tests/system-menu-page-contract.test.mjs                                  |   37 ++
 12 files changed, 591 insertions(+), 197 deletions(-)

diff --git a/rsf-design/.env.development b/rsf-design/.env.development
index 38d70b4..36ddc4b 100644
--- a/rsf-design/.env.development
+++ b/rsf-design/.env.development
@@ -7,7 +7,7 @@
 VITE_API_URL = /
 
 # 浠g悊鐩爣鍦板潃锛堝紑鍙戠幆澧冮�氳繃 Vite 浠g悊杞彂璇锋眰鍒版鍦板潃锛岃В鍐宠法鍩熼棶棰橈級
-VITE_API_PROXY_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+VITE_API_PROXY_URL = http://127.0.0.1:8085/ref-server
 
 # Delete console
 VITE_DROP_CONSOLE = false
diff --git a/rsf-design/src/api/system-manage.js b/rsf-design/src/api/system-manage.js
index e5c5bc3..1c9f35a 100644
--- a/rsf-design/src/api/system-manage.js
+++ b/rsf-design/src/api/system-manage.js
@@ -1,19 +1,132 @@
 import request from '@/utils/http'
+
+export function buildUserListParams(params = {}) {
+  return {
+    current: params.current || 1,
+    pageSize: params.pageSize || params.size || 20,
+    username: params.username,
+    nickname: params.nickname,
+    phone: params.phone,
+    status: params.status,
+    deptId: params.deptId
+  }
+}
+
+export function buildRoleListParams(params = {}) {
+  return {
+    current: params.current || 1,
+    pageSize: params.pageSize || params.size || 20,
+    name: params.name,
+    code: params.code,
+    memo: params.memo,
+    status: params.status
+  }
+}
+
 function fetchGetUserList(params) {
-  return request.get({
-    url: '/api/user/list',
-    params
-  })
+  return request.post({ url: '/user/page', params: buildUserListParams(params) })
 }
+
+function fetchSaveUser(params) {
+  return request.post({ url: '/user/save', params })
+}
+
+function fetchUpdateUser(params) {
+  return request.post({ url: '/user/update', params })
+}
+
+function fetchDeleteUser(id) {
+  return request.post({ url: `/user/remove/${id}` })
+}
+
+function fetchResetUserPassword(params) {
+  return request.post({ url: '/auth/reset/password', params })
+}
+
+function fetchUpdateUserStatus(params) {
+  return request.post({ url: '/user/update', params })
+}
+
+function fetchGetUserDetail(id) {
+  return request.get({ url: `/user/${id}` })
+}
+
 function fetchGetRoleList(params) {
-  return request.get({
-    url: '/api/role/list',
+  return request.post({ url: '/role/page', params: buildRoleListParams(params) })
+}
+
+function fetchSaveRole(params) {
+  return request.post({ url: '/role/save', params })
+}
+
+function fetchUpdateRole(params) {
+  return request.post({ url: '/role/update', params })
+}
+
+function fetchDeleteRole(id) {
+  return request.post({ url: `/role/remove/${id}` })
+}
+
+function fetchGetRoleOptions(params) {
+  return request.post({ url: '/role/list', params })
+}
+
+function fetchGetDeptTree(params) {
+  return request.post({ url: '/dept/tree', params })
+}
+
+function fetchGetMenuTree(params) {
+  return request.post({ url: '/menu/tree', params })
+}
+
+function fetchGetRoleScopeList(scopeType, roleId) {
+  const urlMap = {
+    menu: '/role/scope/list',
+    pda: '/rolePda/scope/list',
+    matnr: '/roleMatnr/scope/list',
+    warehouse: '/roleWarehouse/scope/list'
+  }
+  return request.get({ url: urlMap[scopeType], params: { roleId } })
+}
+
+function fetchUpdateRoleScope(scopeType, params) {
+  const urlMap = {
+    menu: '/role/scope/update',
+    pda: '/rolePda/scope/update',
+    matnr: '/roleMatnr/scope/update',
+    warehouse: '/roleWarehouse/scope/update'
+  }
+  return request.post({ url: urlMap[scopeType], params })
+}
+
+function fetchGetUserLoginList(params) {
+  return request.post({
+    url: '/userLogin/page',
     params
   })
 }
-function fetchGetMenuList() {
-  return request.get({
-    url: '/api/v3/system/menus/simple'
-  })
+
+function fetchGetMenuList(params) {
+  return fetchGetMenuTree(params)
 }
-export { fetchGetMenuList, fetchGetRoleList, fetchGetUserList }
+
+export {
+  fetchGetUserList,
+  fetchSaveUser,
+  fetchUpdateUser,
+  fetchDeleteUser,
+  fetchResetUserPassword,
+  fetchUpdateUserStatus,
+  fetchGetUserDetail,
+  fetchGetRoleList,
+  fetchSaveRole,
+  fetchUpdateRole,
+  fetchDeleteRole,
+  fetchGetRoleOptions,
+  fetchGetDeptTree,
+  fetchGetMenuTree,
+  fetchGetRoleScopeList,
+  fetchUpdateRoleScope,
+  fetchGetUserLoginList,
+  fetchGetMenuList
+}
diff --git a/rsf-design/src/locales/langs/en.json b/rsf-design/src/locales/langs/en.json
index 4dbade2..e9ee97d 100644
--- a/rsf-design/src/locales/langs/en.json
+++ b/rsf-design/src/locales/langs/en.json
@@ -265,6 +265,115 @@
       "menu": "Menu Manage"
     }
   },
+  "menu": {
+    "basStationArea": "BasStationArea",
+    "dashboard": "Dashboard",
+    "settings": "Settings",
+    "basicInfo": "BasicInfo",
+    "system": "System",
+    "user": "User",
+    "role": "Role",
+    "menu": "Menu",
+    "host": "Host",
+    "department": "Department",
+    "token": "Token",
+    "operation": "Operation",
+    "config": "Config",
+    "aiParam": "AI Params",
+    "aiPrompt": "Prompts",
+    "aiMcpMount": "MCP Mounts",
+    "aiCallLog": "AI Observe",
+    "tenant": "Tenant",
+    "userLogin": "Token",
+    "customer": "Customer",
+    "shipper": "shipper",
+    "matnr": "Matnr",
+    "matnrGroup": "MatnrGroup",
+    "warehouse": "Warehouse",
+    "warehouseAreas": "WarehouseAreas",
+    "loc": "Loc",
+    "locItem": "LocItem",
+    "locType": "LocType",
+    "locArea": "locArea",
+    "locAreaMat": "Logic Areas",
+    "locAreaMatRela": "LocAreaMatRela",
+    "container": "Container",
+    "contract": "Contract",
+    "qlyInspect": "QlyInspect",
+    "qlyIsptItem": "qlyIsptItem",
+    "dictType": "DictType",
+    "dictData": "DictData",
+    "companys": "Companys",
+    "serialRuleItem": "SerialRuleItem",
+    "serialRule": "SerialRule",
+    "asnOrder": "AsnOrder",
+    "asnOrderItem": "AsnOrderItem",
+    "asnOrderLog": "asnOrderLog",
+    "asnOrderItemLog": "asnOrderItemLog",
+    "purchase": "Purchase",
+    "purchaseItem": "PurchaseItem",
+    "whMat": "Warehouse Mat",
+    "fields": "Extend Fields",
+    "fieldsItem": "Extend Fields Items",
+    "warehouseAreasItem": "Temp Warehouse Areas Stock",
+    "deviceSite": "deviceSite",
+    "waitPakin": "WaitPakin",
+    "waitPakinItem": "WaitPakinItem",
+    "task": "Task",
+    "taskItem": "TaskItem",
+    "taskLog": "TaskLog",
+    "taskItemLog": "TaskItemLog",
+    "stock": "Stock Manage",
+    "stockItem": "Stock Item",
+    "locPreview": "LocItem",
+    "histories": "Histories",
+    "wareWork": "Warehouse Working",
+    "statistics": "Stock Statistics",
+    "stockManage": "Stock Manage",
+    "logs": "Logs",
+    "permissions": "Permissions",
+    "delivery": "Delivery",
+    "outStock": "Out Stock",
+    "outStockItem": "Out Stock Item",
+    "inStockPoces": "In Stock Pocess",
+    "outStockPoces": "Out Stock Pocess",
+    "warehouseStock": "Instant Inventory",
+    "deviceBind": "Device Bind",
+    "tasks": "Tasks",
+    "wave": "Wave Manage",
+    "basStation": "BasStation",
+    "basContainer": "BasContainer",
+    "outBound": "Out Bound",
+    "checkOutBound": "Check Out Bound",
+    "stockTransfer": "Stock Transfer",
+    "waveRule": "Wave Rules",
+    "checkOrder": "Check Order",
+    "checkDiff": "Check Diff",
+    "transfer": "Transfer",
+    "transferItem": "Transfer Item",
+    "locRevise": "Loc Revise",
+    "statisticReport": "Statistical Report",
+    "locDeadReport": "Locs Dead Report",
+    "stockStatistic": "Stock Statistic",
+    "outStatistic": "Out Statistic",
+    "inStatistic": "In Statistic",
+    "inStatisticItem": "In Statistic Item",
+    "outStatisticItem": "Out Statistic Item",
+    "statisticCount": "Statistic Count",
+    "preparation": "Preparation",
+    "check": "Check",
+    "abnormal": "Abnormal",
+    "platform": "Platform",
+    "freeze": "Freeze",
+    "transferPoces": "Transfer Process",
+    "menuPda": "MenuPda",
+    "taskPathTemplate": "TaskPathTemplate",
+    "taskPathTemplateNode": "TaskPathTemplateNode",
+    "subsystemFlowTemplate": "SubsystemFlowTemplate",
+    "flowStepTemplate": "FlowStepTemplate",
+    "taskPathTemplateMerge": "TaskPathTemplateMerge",
+    "missionFlowStepInstance": "Mission Flow Steps"
+  },
   "table": {
     "form": {
       "reset": "Reset",
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
index 2e9bdcc..c72e9ea 100644
--- a/rsf-design/src/locales/langs/zh.json
+++ b/rsf-design/src/locales/langs/zh.json
@@ -265,6 +265,117 @@
       "menu": "鑿滃崟绠$悊"
     }
   },
+  "menu": {
+    "basStationArea": "绔欑偣鍖哄煙",
+    "dashboard": "鎺у埗鍙�",
+    "settings": "涓汉璁剧疆",
+    "basicInfo": "鍩虹淇℃伅",
+    "system": "绯荤粺璁剧疆",
+    "user": "鐢ㄦ埛绠$悊",
+    "role": "瑙掕壊绠$悊",
+    "menu": "鑿滃崟绠$悊",
+    "host": "鏈烘瀯绠$悊",
+    "department": "閮ㄩ棬绠$悊",
+    "token": "鐧诲綍鏃ュ織",
+    "operation": "鎿嶄綔鏃ュ織",
+    "config": "閰嶇疆鍙傛暟",
+    "aiParam": "AI 鍙傛暟",
+    "aiPrompt": "Prompt 绠$悊",
+    "aiMcpMount": "MCP 鎸傝浇",
+    "aiCallLog": "AI 瑙傛祴",
+    "tenant": "绉熸埛绠$悊",
+    "userLogin": "鐧诲綍鏃ュ織",
+    "customer": "瀹㈡埛琛�",
+    "shipper": "璐т富淇℃伅",
+    "matnr": "鐗╂枡",
+    "matnrGroup": "鐗╂枡鍒嗙粍",
+    "warehouse": "浠撳簱",
+    "warehouseAreas": "搴撳尯",
+    "loc": "搴撲綅",
+    "locItem": "搴撳瓨鏄庣粏",
+    "locType": "搴撲綅绫诲瀷(搴�)",
+    "locArea": "閫昏緫鍒嗗尯(搴�)",
+    "locAreaMat": "閫昏緫鍒嗗尯",
+    "locAreaMatRela": "搴撳尯鐗╂枡鍏崇郴",
+    "container": "瀹瑰櫒绠$悊(搴�)",
+    "contract": "鍚堝悓淇℃伅(搴�)",
+    "qlyInspect": "璐ㄦ淇℃伅",
+    "qlyIsptItem": "璐ㄦ淇℃伅鏄庣粏",
+    "dictType": "鏁版嵁瀛楀吀",
+    "dictData": "瀛楀吀鏁版嵁闆�",
+    "companys": "寰�鏉ヤ紒涓�",
+    "serialRuleItem": "缂栫爜瑙勫垯瀛愯〃",
+    "serialRule": "缂栫爜瑙勫垯",
+    "asnOrder": "鍏ュ簱閫氱煡鍗�",
+    "asnOrderItem": "鏀惰揣鏄庣粏",
+    "asnOrderLog": "鍘嗗彶閫氱煡鍗�",
+    "asnOrderItemLog": "鏀惰揣鍘嗗彶鏄庣粏",
+    "purchase": "PO鍗�",
+    "purchaseItem": "PO鍗曟槑缁�",
+    "whMat": "搴撳尯鐗╂枡鍏崇郴",
+    "fields": "鎵╁睍瀛楁",
+    "fieldsItem": "鎵╁睍瀛楁鏄庣粏",
+    "warehouseAreasItem": "鏀惰揣搴撳瓨",
+    "deviceSite": "璺緞绠$悊",
+    "waitPakin": "缁勬墭妗�",
+    "waitPakinItem": "缁勬墭妗f槑缁�",
+    "waitPakinLog": "缁勬墭鍘嗗彶妗�",
+    "waitPakinItemLog": "缁勬墭鍘嗗彶妗f槑缁�",
+    "task": "浠诲姟绠$悊",
+    "taskItem": "浠诲姟妗f槑缁�",
+    "taskLog": "浠诲姟鍘嗗彶妗�",
+    "taskItemLog": "浠诲姟鏄庣粏鍘嗗彶妗�",
+    "stock": "鍏ュ嚭搴撳巻鍙�",
+    "stockItem": "鍗曟嵁鏄庣粏",
+    "locPreview": "搴撲綅鏄庣粏",
+    "histories": "鍘嗗彶妗�",
+    "wareWork": "浠撳簱浣滀笟",
+    "statistics": "搴撳瓨鏌ヨ",
+    "stockManage": "搴撳瓨绠$悊",
+    "logs": "鏃ュ織",
+    "permissions": "鏉冮檺绠$悊",
+    "delivery": "DO鍗�",
+    "outStock": "鍑哄簱閫氱煡鍗�",
+    "outStockItem": "鍑哄簱鍗曟槑缁�",
+    "inStockPoces": "鍏ュ簱绠$悊",
+    "outStockPoces": "鍑哄簱绠$悊",
+    "warehouseStock": "鍗虫椂搴撳瓨",
+    "deviceBind": "璁惧缁戝畾",
+    "tasks": "浠诲姟绠$悊",
+    "wave": "娉㈡绠$悊",
+    "basStation": "绔欑偣绠$悊",
+    "basContainer": "瀹瑰櫒瑙勫垯",
+    "outBound": "鍑哄簱浣滀笟",
+    "checkOutBound": "鐩樼偣鍑哄簱",
+    "stockTransfer": "搴撲綅杞Щ",
+    "waveRule": "娉㈡绛栫暐",
+    "checkOrder": "鐩樼偣鍗�",
+    "checkDiff": "鐩樼偣宸紓鍗�",
+    "transfer": "璋冩嫈鍗�",
+    "transferItem": "璋冩嫈鍗曟槑缁�",
+    "locRevise": "搴撳瓨璋冩暣",
+    "statisticReport": "鎶ヨ〃绠$悊",
+    "locDeadReport": "搴撳瓨鍋滄粸鎶ヨ〃",
+    "stockStatistic": "鏃ュ叆搴撴眹鎬绘煡璇�",
+    "outStatistic": "鏃ュ嚭搴撴眹鎬绘煡璇�",
+    "inStatistic": "鏃ュ叆搴撴眹鎬绘煡璇�",
+    "inStatisticItem": "鏃ュ叆搴撴槑缁嗘煡璇�",
+    "outStatisticItem": "鏃ュ嚭搴撴槑缁嗘煡璇�",
+    "statisticCount": "鏃ュ嚭鍏ュ簱姹囨�荤粺璁�",
+    "preparation": "澶囨枡鍗�",
+    "check": "鐩樼偣绠$悊",
+    "abnormal": "寮傚父绠$悊",
+    "platform": "骞冲彴绠$悊",
+    "freeze": "搴撳瓨鍐荤粨",
+    "transferPoces": "璋冩嫧绠$悊",
+    "menuPda": "PDA鑿滃崟",
+    "taskPathTemplate": "浠诲姟璺緞妯℃澘",
+    "taskPathTemplateNode": "浠诲姟璺緞妯℃澘鑺傜偣",
+    "subsystemFlowTemplate": "瀛愮郴缁熸祦绋嬫ā鏉�",
+    "flowStepTemplate": "娴佺▼姝ラ妯℃澘",
+    "taskPathTemplateMerge": "浠诲姟璺緞妯℃澘鍚堝苟",
+    "missionFlowStepInstance": "浠诲姟娴佺▼姝ラ"
+  },
   "table": {
     "form": {
       "reset": "閲嶇疆",
diff --git a/rsf-design/src/utils/router.js b/rsf-design/src/utils/router.js
index 9f8b47e..7c5d64f 100644
--- a/rsf-design/src/utils/router.js
+++ b/rsf-design/src/utils/router.js
@@ -20,10 +20,17 @@
 }
 const formatMenuTitle = (title) => {
   if (title) {
-    if (title.startsWith('menus.')) {
+    if (title.startsWith('menus.') || title.startsWith('menu.')) {
       if (i18n.global.te(title)) {
         return $t(title)
       } else {
+        const fallbackTitle =
+          title.startsWith('menus.') && title.split('.').pop()
+            ? `menu.${title.split('.').pop()}`
+            : ''
+        if (fallbackTitle && i18n.global.te(fallbackTitle)) {
+          return $t(fallbackTitle)
+        }
         return title.split('.').pop() || title
       }
     }
diff --git a/rsf-design/src/views/system/menu/index.vue b/rsf-design/src/views/system/menu/index.vue
index d28afa8..3a79c27 100644
--- a/rsf-design/src/views/system/menu/index.vue
+++ b/rsf-design/src/views/system/menu/index.vue
@@ -53,6 +53,7 @@
   import MenuDialog from './modules/menu-dialog.vue'
 
   import { formatMenuTitle } from '@/utils/router'
+  import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
   import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
   import { useTableColumns } from '@/hooks/core/useTableColumns'
   import { fetchGetMenuList } from '@/api/system-manage'
@@ -115,12 +116,34 @@
     if (row.meta?.link) return '澶栭摼'
     return '鏈煡'
   }
+  const getMenuDisplayTitle = (row) => {
+    const titleKey = row.meta?.title || row.name || ''
+    const normalizedTitleKey =
+      titleKey && !String(titleKey).includes('.') ? `menu.${titleKey}` : titleKey
+    return formatMenuTitle(normalizedTitleKey)
+  }
+  const getMenuDisplayIcon = (row) => row.meta?.icon || row.icon || ''
   const { columnChecks, columns } = useTableColumns(() => [
+    {
+      prop: 'meta.icon',
+      label: '鍥炬爣棰勮',
+      width: 96,
+      align: 'center',
+      formatter: (row) => {
+        const icon = getMenuDisplayIcon(row)
+
+        if (!icon) return h('span', { class: 'text-g-400' }, '-')
+
+        return h('div', { class: 'flex items-center justify-center' }, [
+          h(ArtSvgIcon, { icon, class: 'text-base text-g-700' })
+        ])
+      }
+    },
     {
       prop: 'meta.title',
       label: '鑿滃崟鍚嶇О',
       minWidth: 120,
-      formatter: (row) => formatMenuTitle(row.meta?.title)
+      formatter: (row) => getMenuDisplayTitle(row)
     },
     {
       prop: 'type',
@@ -245,11 +268,11 @@
     })
   }
   const searchMenu = (items) => {
-    const results = []
+      const results = []
     for (const item of items) {
       const searchName = appliedFilters.name?.toLowerCase().trim() || ''
       const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
-      const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
+      const menuTitle = getMenuDisplayTitle(item).toLowerCase()
       const menuPath = (item.path || '').toLowerCase()
       const nameMatch = !searchName || menuTitle.includes(searchName)
       const routeMatch = !searchRoute || menuPath.includes(searchRoute)
diff --git a/rsf-design/tests/system-manage-contract.test.mjs b/rsf-design/tests/system-manage-contract.test.mjs
new file mode 100644
index 0000000..c9879e9
--- /dev/null
+++ b/rsf-design/tests/system-manage-contract.test.mjs
@@ -0,0 +1,20 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import { buildUserListParams, buildRoleListParams } from '../src/api/system-manage.js'
+
+test('buildUserListParams matches the rsf-admin paging contract', () => {
+  assert.deepEqual(
+    buildUserListParams({
+      current: 2,
+      pageSize: 20,
+      username: 'root',
+      deptId: 3
+    }),
+    {
+      current: 2,
+      pageSize: 20,
+      username: 'root',
+      deptId: 3
+    }
+  )
+})
diff --git a/rsf-design/tests/system-menu-page-contract.test.mjs b/rsf-design/tests/system-menu-page-contract.test.mjs
new file mode 100644
index 0000000..d8bb894
--- /dev/null
+++ b/rsf-design/tests/system-menu-page-contract.test.mjs
@@ -0,0 +1,37 @@
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+import test from 'node:test'
+
+const projectRoot = path.resolve(import.meta.dirname, '..')
+const zhLocalePath = path.join(projectRoot, 'src', 'locales', 'langs', 'zh.json')
+const enLocalePath = path.join(projectRoot, 'src', 'locales', 'langs', 'en.json')
+const routerUtilsPath = path.join(projectRoot, 'src', 'utils', 'router.js')
+const menuPagePath = path.join(projectRoot, 'src', 'views', 'system', 'menu', 'index.vue')
+
+test('current-system menu keys are available in rsf-design locales', () => {
+  const zhMessages = JSON.parse(fs.readFileSync(zhLocalePath, 'utf8'))
+  const enMessages = JSON.parse(fs.readFileSync(enLocalePath, 'utf8'))
+
+  assert.equal(zhMessages.menu?.system, '绯荤粺璁剧疆')
+  assert.equal(zhMessages.menu?.basicInfo, '鍩虹淇℃伅')
+  assert.equal(zhMessages.menu?.aiParam, 'AI 鍙傛暟')
+
+  assert.equal(enMessages.menu?.system, 'System')
+  assert.equal(enMessages.menu?.basicInfo, 'BasicInfo')
+  assert.equal(enMessages.menu?.aiParam, 'AI Params')
+})
+
+test('formatMenuTitle recognizes current-system menu translation keys', () => {
+  const routerSource = fs.readFileSync(routerUtilsPath, 'utf8')
+
+  assert.match(routerSource, /startsWith\('menu\.'\)|startsWith\("menu\."\)/)
+})
+
+test('menu management table shows icon preview and translated names', () => {
+  const menuPageSource = fs.readFileSync(menuPagePath, 'utf8')
+
+  assert.match(menuPageSource, /label:\s*'鍥炬爣棰勮'/)
+  assert.match(menuPageSource, /ArtSvgIcon/)
+  assert.match(menuPageSource, /row\.meta\?\.title\s*\|\|\s*row\.name|item\.meta\?\.title\s*\|\|\s*item\.name/)
+})
diff --git a/rsf-design/tests/system-role-scope-contract.test.mjs b/rsf-design/tests/system-role-scope-contract.test.mjs
new file mode 100644
index 0000000..30c293e
--- /dev/null
+++ b/rsf-design/tests/system-role-scope-contract.test.mjs
@@ -0,0 +1,21 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import {
+  buildScopeTreeNodes,
+  buildScopeSavePayload,
+  SCOPE_TYPES
+} from '../src/views/system/role/roleScope.config.js'
+
+test('menu scope nodes preserve backend ids for save', () => {
+  const tree = buildScopeTreeNodes(SCOPE_TYPES.menu, [{ id: 1, label: '绯荤粺绠$悊' }])
+  assert.equal(tree[0].id, 1)
+})
+
+test('scope save payload is delegated per scope type', () => {
+  const payload = buildScopeSavePayload(SCOPE_TYPES.menu, {
+    roleId: 9,
+    selectedIds: [1, 2],
+    authType: 0
+  })
+  assert.equal(payload.id, 9)
+})
diff --git a/rsf-design/tests/system-user-page-contract.test.mjs b/rsf-design/tests/system-user-page-contract.test.mjs
new file mode 100644
index 0000000..3c09037
--- /dev/null
+++ b/rsf-design/tests/system-user-page-contract.test.mjs
@@ -0,0 +1,10 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import { buildUserDialogModel } from '../src/views/system/user/userPage.helpers.js'
+
+test('buildUserDialogModel maps rsf-admin edit data into the dialog model', () => {
+  assert.equal(
+    buildUserDialogModel({ username: 'root', nickname: '绠$悊鍛�', deptId: 1, roles: [{ id: 3 }] }).username,
+    'root'
+  )
+})
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java
index d6c0af5..4a2b8c7 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java
@@ -11,6 +11,8 @@
 
 import java.util.Date;
 import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * redis tools
@@ -60,6 +62,31 @@
 		return null;
 	}
 
+	private <T> T withJedis(Function<Jedis, T> action) {
+		Jedis jedis = this.getJedis();
+		if (jedis == null) {
+			return null;
+		}
+		try (jedis) {
+			return action.apply(jedis);
+		} catch (Exception e) {
+			log.error(this.getClass().getSimpleName(), e);
+		}
+		return null;
+	}
+
+	private void withJedisVoid(Consumer<Jedis> action) {
+		Jedis jedis = this.getJedis();
+		if (jedis == null) {
+			return;
+		}
+		try (jedis) {
+			action.accept(jedis);
+		} catch (Exception e) {
+			log.error(this.getClass().getSimpleName(), e);
+		}
+	}
+
 	// key - object ----------------------------------------------------------------------------------------------------------
 
 	public String set(String flag, String key, Object value) {
@@ -70,13 +97,7 @@
 			this.delete(flag, key);
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.set((flag + LINK + key).getBytes(), Serialize.serialize(value));
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.set((flag + LINK + key).getBytes(), Serialize.serialize(value)));
 	}
 
 	public String set(String flag, String key, Object value, Integer seconds) {
@@ -87,58 +108,38 @@
 			this.delete(flag, key);
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.setex((flag + LINK + key).getBytes(), seconds, Serialize.serialize(value));
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.setex((flag + LINK + key).getBytes(), seconds, Serialize.serialize(value)));
 	}
 
 	public <T> T get(String flag, String key) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		return withJedis(jedis -> {
 			byte[] bytes = jedis.get((flag + LINK + key).getBytes());
 			if(bytes == null || bytes.length == 0 ) {
 				return null;
 			}
 			return (T) Serialize.unSerialize(bytes);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		});
 	}
 
 	public Long delete(String flag, String key) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.del((flag + LINK + key).getBytes());
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.del((flag + LINK + key).getBytes()));
 	}
 
 	public Long clear(String flag) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
 		this.setValue(flag, "CLEARING", "true");
-		try{
+		return withJedis(jedis -> {
 			Object returnValue = jedis.eval("local keys = redis.call('keys', ARGV[1]) for i=1,#keys,1000 do redis.call('del', unpack(keys, i, math.min(i+4999, #keys))) end return #keys",0,flag + LINK + "*");
 			return Long.parseLong(String.valueOf(returnValue));
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		});
 	}
 
 	// 涓哄凡瀛樺湪鐨刱ey璁剧疆杩囨湡鏃堕棿 - 绉�
@@ -146,12 +147,9 @@
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expire((flag + LINK + key).getBytes(), seconds);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 	// 涓哄凡瀛樺湪鐨刱ey璁剧疆杩囨湡鏃堕棿 - 鍏蜂綋鍒版椂闂存埑 锛堢锛�
@@ -159,12 +157,9 @@
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expireAt((flag + LINK + key).getBytes(), toTime.getTime()/1000);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 	// 鑾峰彇杩囨湡鍓╀綑鏃堕棿锛堢锛� ttl == -1 娌℃湁璁剧疆杩囨湡鏃堕棿锛� ttl == -2 key涓嶅瓨鍦�
@@ -172,13 +167,7 @@
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.ttl((flag + LINK + key).getBytes());
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.ttl((flag + LINK + key).getBytes()));
 	}
 
 
@@ -188,39 +177,21 @@
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.set(flag + LINK + key, value);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.set(flag + LINK + key, value));
 	}
 
 	public String setValue(String flag, String key, String value, Integer seconds) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.setex(flag + LINK + key, seconds , value);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.setex(flag + LINK + key, seconds , value));
 	}
 
 	public String getValue(String flag, String key) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.get(flag + LINK + key);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.get(flag + LINK + key));
 	}
 
 	public Long deleteValue(String flag, String... key) {
@@ -228,17 +199,13 @@
 			return null;
 		}
 
-		Jedis jedis = this.getJedis();
-		try{
+		return withJedis(jedis -> {
 			String[] keys = new String[key.length];
 			for(int i=0;i<key.length;i++){
 				keys[i] = flag + LINK + key[i];
 			}
 			return jedis.del(keys);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		});
 	}
 
 	public Long clearValue(String flag) {
@@ -247,39 +214,28 @@
 		}
 
 		this.setValue(flag, "CLEARING", "true");
-		Jedis jedis = this.getJedis();
-
-		try{
+		return withJedis(jedis -> {
 			Object returnValue = jedis.eval("return redis.call('del', unpack(redis.call('keys', ARGV[1])))",0,flag + LINK + "*");
 			return Long.parseLong(String.valueOf(returnValue));
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		});
 	}
 
 	public void setValueExpire(String flag, String key,int seconds){
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expire((flag + LINK + key).getBytes(), seconds);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 	public void setValueExpireAt(String flag, String key,Date atTime){
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expireAt((flag + LINK + key).getBytes(), atTime.getTime()/1000);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 
@@ -293,95 +249,63 @@
 			deleteMap(name,key);
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try {
-			return jedis.hset(name.getBytes(), key.getBytes(), Serialize.serialize(value));
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.hset(name.getBytes(), key.getBytes(), Serialize.serialize(value)));
 	}
 
 	public <T> T getMap(String name, String key) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		return withJedis(jedis -> {
 			byte[] bytes = jedis.hget(name.getBytes(), key.getBytes());
 			if (bytes == null || bytes.length == 0) {
 				return null;
 			}
 			return (T) Serialize.unSerialize(bytes);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		});
 	}
 
 	public Set<String> getMapKeys(String name) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-            return jedis.hkeys(name);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.hkeys(name));
 	}
 
 	public Long deleteMap(String name, String... key) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		return withJedis(jedis -> {
 			String[] keys = new String[key.length];
             System.arraycopy(key, 0, keys, 0, key.length);
             return jedis.hdel(name, keys);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		});
 	}
 
 	public Long clearMap(String name) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.del(name);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.del(name));
 	}
 
 	public void setMapExpire(String name,int seconds){
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expire(name.getBytes(), seconds);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 	public void setMapExpireAt(String name,Date atTime){
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expireAt(name.getBytes(), atTime.getTime()/1000);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 
@@ -395,13 +319,7 @@
 		if(value == null){
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.rpush(name.getBytes(), Serialize.serialize(value));
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.rpush(name.getBytes(), Serialize.serialize(value)));
 	}
 
 	// 鑾峰彇鍒楄〃澶撮儴鍏冪礌 && 鍒犻櫎
@@ -409,17 +327,13 @@
 		if(!this.initialize){
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		return withJedis(jedis -> {
 			byte[] bytes = jedis.lpop(name.getBytes());
 			if(bytes == null || bytes.length == 0) {
 				return null;
 			}
 			return (T) Serialize.unSerialize(bytes);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		});
 	}
 
 	// 鍒犻櫎
@@ -427,37 +341,25 @@
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-			return jedis.del(name);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.del(name));
 	}
 
 	public void setListExpire(String name, int seconds){
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expire(name.getBytes(), seconds);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 	public void setListExpireAt(String name, Date atTime){
 		if(!this.initialize) {
 			return;
 		}
-		Jedis jedis = this.getJedis();
-		try{
+		withJedisVoid(jedis -> {
 			jedis.expireAt(name.getBytes(), atTime.getTime()/1000);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
+		});
 	}
 
 	// count ----------------------------------------------------------------------------------------------------------
@@ -466,26 +368,14 @@
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-            return jedis.incr("COUNT." + key);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.incr("COUNT." + key));
 	}
 
 	public Long decr(String key) {
 		if(!this.initialize) {
 			return null;
 		}
-		Jedis jedis = this.getJedis();
-		try{
-            return jedis.decr("COUNT." + key);
-		} catch (Exception e) {
-			log.error(this.getClass().getSimpleName(), e);
-		}
-		return null;
+		return withJedis(jedis -> jedis.decr("COUNT." + key));
 	}
 
 }
diff --git a/rsf-server/src/test/java/com/vincent/rsf/server/common/service/RedisServiceTest.java b/rsf-server/src/test/java/com/vincent/rsf/server/common/service/RedisServiceTest.java
new file mode 100644
index 0000000..c61c678
--- /dev/null
+++ b/rsf-server/src/test/java/com/vincent/rsf/server/common/service/RedisServiceTest.java
@@ -0,0 +1,53 @@
+package com.vincent.rsf.server.common.service;
+
+import com.vincent.rsf.common.utils.Serialize;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import redis.clients.jedis.Jedis;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RedisServiceTest {
+
+    @Mock
+    private Jedis jedis;
+
+    private RedisService redisService;
+
+    @BeforeEach
+    void setUp() {
+        redisService = new RedisService() {
+            @Override
+            public Jedis getJedis() {
+                return jedis;
+            }
+        };
+        redisService.initialize = true;
+    }
+
+    @Test
+    void getClosesBorrowedJedisAfterReadingValue() {
+        when(jedis.get("MENU_TREE.FULL_TREE".getBytes())).thenReturn(Serialize.serialize("cached"));
+
+        String value = redisService.get("MENU_TREE", "FULL_TREE");
+
+        assertEquals("cached", value);
+        verify(jedis).close();
+    }
+
+    @Test
+    void deleteClosesBorrowedJedisAfterDeletingKey() {
+        when(jedis.del("MENU_TREE.FULL_TREE".getBytes())).thenReturn(1L);
+
+        Long deleted = redisService.delete("MENU_TREE", "FULL_TREE");
+
+        assertEquals(1L, deleted);
+        verify(jedis).close();
+    }
+}

--
Gitblit v1.9.1