From d5923df26e6dcbb0ebc089d9b021630dc7535300 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Fri, 11 Sep 2026 22:38:13 +0800 Subject: [PATCH] docs: add mini-db mobile menu permission implementation plan. Break down SQL grant, workbench filter helper, and store wiring into executable tasks. Co-authored-by: Cursor --- .../2026-09-11-mini-db-menu-permission.md | 485 ++++++++++++++++++ 1 file changed, 485 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-11-mini-db-menu-permission.md diff --git a/docs/superpowers/plans/2026-09-11-mini-db-menu-permission.md b/docs/superpowers/plans/2026-09-11-mini-db-menu-permission.md new file mode 100644 index 000000000..7df9e8c05 --- /dev/null +++ b/docs/superpowers/plans/2026-09-11-mini-db-menu-permission.md @@ -0,0 +1,485 @@ +# 小程序地磅移动端菜单权限 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 `sys_menu`「移动端」下挂载地磅管理四菜单(`mini:db:*`),测试库仅授权超级管理员;小程序工作台按 `GET /web/auth/menu-codes` 过滤展示。 + +**Architecture:** 幂等 SQL 插入菜单 + `sys_role_menu`;前端保留静态 `workbench-menus.js` 并增加 `perms`;纯函数按 `menuAll` / `menuCodes` 过滤模块;`user` store 拉取并缓存权限码。不新增后端菜单接口、不改地磅 API 鉴权。 + +**Tech Stack:** MySQL 5.7、`sys_menu` / `sys_role_menu`、uni-app + Pinia、现有 `/web/auth/menu-codes` + +**Spec:** `docs/superpowers/specs/2026-09-11-mini-db-menu-permission-design.md` + +--- + +## 文件结构 + +| 操作 | 文件 | 职责 | +|------|------|------| +| Create | `docs/sql/migrations/2026-09-11-mini-db-menus.sql` | 幂等插入 5 个菜单 + 超级管理员 `sys_role_menu` | +| Create | `miniprogram/utils/workbenchMenuFilter.js` | 纯函数:按权限过滤工作台模块 | +| Modify | `miniprogram/config/workbench-menus.js` | 地磅叶子增加 `perms` | +| Modify | `miniprogram/config/api.js` | `AUTH_API.MENU_CODES` | +| Modify | `miniprogram/stores/user.js` | `menuAll` / `menuCodes` + `fetchMenuCodes`;登出清空 | +| Modify | `miniprogram/pages/workbench/workbench.vue` | `onShow` 拉权限;用过滤后的 `modules` 渲染 | + +**不做:** `/mini/auth/menu-codes`、仪表盘快捷入口裁剪、地磅 API `mini:*` 硬鉴权、其它业务移动端子树。 + +--- + +### Task 1: 幂等 SQL 迁移脚本 + +**Files:** +- Create: `docs/sql/migrations/2026-09-11-mini-db-menus.sql` + +- [ ] **Step 1: 创建迁移文件** + +内容必须完整如下(注意:`sys_role_menu.id` 用确定性 `MD5`,禁止多行共用 `UUID()`): + +```sql +-- 小程序地磅:移动端菜单 + 超级管理员授权 +-- 设计: docs/superpowers/specs/2026-09-11-mini-db-menu-permission-design.md +-- +-- 结构: +-- 移动端 (id=1) +-- └─ 地磅管理 (MINI_DB_ROOT, M) +-- ├─ 仪表盘 mini:db:dashboard +-- ├─ 计划管理 mini:db:plan:list +-- ├─ 出入库管理 mini:db:inventory:list +-- └─ 地磅单管理 mini:db:weighing:list +-- +-- 授权:仅 role_name='超级管理员';其它角色在角色管理手工勾选。 + +SET NAMES utf8mb4; + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_DB_ROOT', '地磅管理', '1', 'M', '/mini/db', NULL, NULL, + 1, '1', '1', 'shanxia2.0', 1, '小程序地磅模块', + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_DB_ROOT' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_DB_DASHBOARD', '仪表盘', 'MINI_DB_ROOT', 'C', 'dashboard', NULL, 'mini:db:dashboard', + 1, '1', '1', 'shanxia2.0', 1, NULL, + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_DB_DASHBOARD' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_DB_PLANS', '计划管理', 'MINI_DB_ROOT', 'C', 'plans', NULL, 'mini:db:plan:list', + 1, '1', '1', 'shanxia2.0', 2, NULL, + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_DB_PLANS' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_DB_INVENTORY', '出入库管理', 'MINI_DB_ROOT', 'C', 'inventory', NULL, 'mini:db:inventory:list', + 1, '1', '1', 'shanxia2.0', 3, NULL, + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_DB_INVENTORY' AND `del_flag` = 0 +); + +INSERT INTO `sys_menu` ( + `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, + `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + 'MINI_DB_WEIGHING', '地磅单管理', 'MINI_DB_ROOT', 'C', 'weighing', NULL, 'mini:db:weighing:list', + 1, '1', '1', 'shanxia2.0', 4, NULL, + 'system', '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `sys_menu` WHERE `id` = 'MINI_DB_WEIGHING' AND `del_flag` = 0 +); + +-- 超级管理员:五个节点(含父) +INSERT INTO `sys_role_menu` ( + `id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`, + `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` +) +SELECT + MD5(CONCAT(r.id, '|', m.menu_id, '|mini-db')), + r.id, + m.menu_id, + r.tenant_no, + 0, + 'mini db menus', + 'system', + '', + NOW(), + NULL, + NOW(), + 0 +FROM `sys_role` r +INNER JOIN ( + SELECT 'MINI_DB_ROOT' AS menu_id + UNION ALL SELECT 'MINI_DB_DASHBOARD' + UNION ALL SELECT 'MINI_DB_PLANS' + UNION ALL SELECT 'MINI_DB_INVENTORY' + UNION ALL SELECT 'MINI_DB_WEIGHING' +) m +WHERE r.role_name = '超级管理员' + AND r.tenant_no = 'shanxia2.0' + AND r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM `sys_role_menu` x + WHERE x.role_id = r.id + AND x.menu_id = m.menu_id + AND x.del_flag = 0 + ); +``` + +- [ ] **Step 2: 对测试库执行(使用后端实际连接的库)** + +```bash +cd backend +# 从 .env / .env.production 读取 DATABASE_URL,解析出 host/user/pass/db +# 示例(按本机实际账号替换): +mysql -h -P -u -p'' < ../docs/sql/migrations/2026-09-11-mini-db-menus.sql +``` + +Expected: 无报错。再执行同一脚本一次,仍无报错、无重复行。 + +- [ ] **Step 3: 校验查询** + +```sql +SELECT id, menu_name, parent_id, perms FROM sys_menu +WHERE id LIKE 'MINI_DB_%' AND del_flag = 0 ORDER BY order_num; + +SELECT rm.menu_id, r.role_name +FROM sys_role_menu rm +JOIN sys_role r ON r.id = rm.role_id +WHERE rm.menu_id LIKE 'MINI_DB_%' AND rm.del_flag = 0 AND r.del_flag = 0; +``` + +Expected: 5 行菜单;`sys_role_menu` 仅「超级管理员」关联这 5 个 `menu_id`。 + +- [ ] **Step 4: Commit** + +```bash +git add docs/sql/migrations/2026-09-11-mini-db-menus.sql +git commit -m "$(cat <<'EOF' +feat(sql): add mini-program weighbridge menus under 移动端. + +Insert MINI_DB_* menus with mini:db:* perms and grant them to 超级管理员 only. +EOF +)" +``` + +--- + +### Task 2: 工作台过滤纯函数 + 本地校验 + +**Files:** +- Create: `miniprogram/utils/workbenchMenuFilter.js` + +- [ ] **Step 1: 实现过滤函数** + +```js +/** + * 按菜单权限过滤工作台模块。 + * @param {Array} modules WORKBENCH_MODULES 风格数组 + * @param {{ menuAll?: boolean, menuCodes?: string[] }} auth + * @returns {Array} 过滤后的模块(menus 已裁剪;无叶子的模块去掉) + */ +export function filterWorkbenchModules(modules, auth) { + const list = Array.isArray(modules) ? modules : [] + const menuAll = !!(auth && auth.menuAll) + const codeSet = new Set( + (auth && Array.isArray(auth.menuCodes) ? auth.menuCodes : []) + .map((c) => String(c || '').trim()) + .filter(Boolean) + ) + + return list + .filter((m) => m && m.visible !== false) + .map((m) => { + const menus = Array.isArray(m.menus) ? m.menus : [] + const nextMenus = menus.filter((item) => { + if (!item || item.enabled === false) return false + if (menuAll) return true + const p = item.perms + if (!p) return false + return codeSet.has(String(p)) + }) + return { ...m, menus: nextMenus } + }) + .filter((m) => m.menus.length > 0) +} +``` + +- [ ] **Step 2: 用 node 做快速断言(无独立测试框架时)** + +在仓库根目录执行: + +```bash +node --input-type=module -e " +import { filterWorkbenchModules } from './miniprogram/utils/workbenchMenuFilter.js' +const mods = [{ + key: 'db', visible: true, + menus: [ + { name: '仪表盘', enabled: true, perms: 'mini:db:dashboard' }, + { name: '计划', enabled: true, perms: 'mini:db:plan:list' }, + { name: '关', enabled: false, perms: 'mini:db:x' } + ] +}] +const all = filterWorkbenchModules(mods, { menuAll: true }) +if (all[0].menus.length !== 2) throw new Error('menuAll fail') +const part = filterWorkbenchModules(mods, { menuAll: false, menuCodes: ['mini:db:plan:list'] }) +if (part[0].menus.length !== 1 || part[0].menus[0].name !== '计划') throw new Error('partial fail') +const none = filterWorkbenchModules(mods, { menuAll: false, menuCodes: [] }) +if (none.length !== 0) throw new Error('empty fail') +console.log('ok') +" +``` + +Expected: 打印 `ok`。 + +- [ ] **Step 3: Commit** + +```bash +git add miniprogram/utils/workbenchMenuFilter.js +git commit -m "$(cat <<'EOF' +feat(miniprogram): add workbench menu permission filter helper. + +Pure function filters modules by menuAll or mini:db:* menuCodes. +EOF +)" +``` + +--- + +### Task 3: 静态菜单补 `perms` + AUTH API + +**Files:** +- Modify: `miniprogram/config/workbench-menus.js` +- Modify: `miniprogram/config/api.js` + +- [ ] **Step 1: 地磅叶子增加 perms(仅改 db 模块 menus,其它模块可暂不加)** + +将 `db.menus` 改为: + +```js + menus: [ + { name: '仪表盘', enabled: true, code: 'db-dashboard', icon: '📊', perms: 'mini:db:dashboard' }, + { name: '计划管理', enabled: true, code: 'db-plans', icon: '📋', perms: 'mini:db:plan:list' }, + { name: '出入库管理', enabled: true, code: 'db-inventory', icon: '📦', perms: 'mini:db:inventory:list' }, + { name: '地磅单管理', enabled: true, code: 'db-weighing', icon: '⚖️', perms: 'mini:db:weighing:list' } + ] +``` + +- [ ] **Step 2: `AUTH_API` 增加 MENU_CODES** + +在 `miniprogram/config/api.js` 的 `AUTH_API` 对象中增加: + +```js + MENU_CODES: '/web/auth/menu-codes' +``` + +- [ ] **Step 3: Commit** + +```bash +git add miniprogram/config/workbench-menus.js miniprogram/config/api.js +git commit -m "$(cat <<'EOF' +feat(miniprogram): map workbench db menus to mini:db:* perms. + +Add MENU_CODES auth path for workbench permission loading. +EOF +)" +``` + +--- + +### Task 4: user store 拉取并缓存 menu-codes + +**Files:** +- Modify: `miniprogram/stores/user.js` + +- [ ] **Step 1: 增加状态与 fetch** + +在 store 内 `isLoading` 旁增加: + +```js + const menuAll = ref(false) + const menuCodes = ref([]) +``` + +在 `setLoginInfo` 成功路径之后不强制同步拉码(由工作台 `onShow` / 登录后调用即可)。新增: + +```js + async function fetchMenuCodes() { + if (!token.value || !String(token.value).trim()) { + menuAll.value = false + menuCodes.value = [] + return + } + try { + const res = await request({ + url: AUTH_API.MENU_CODES, + method: 'GET' + }) + const result = parseResponseJson(res) + if (!result || result.code !== 200 || !result.data) { + menuAll.value = false + menuCodes.value = [] + return + } + const data = result.data + menuAll.value = !!(data.all || data.isSuperAdmin) + menuCodes.value = Array.isArray(data.menuCodes) + ? data.menuCodes.map((c) => String(c || '').trim()).filter(Boolean) + : [] + } catch { + menuAll.value = false + menuCodes.value = [] + } + } +``` + +在 `clearLoginInfo` 中增加: + +```js + menuAll.value = false + menuCodes.value = [] +``` + +在 `login` 成功 `setLoginInfo` 之后调用: + +```js + await fetchMenuCodes() +``` + +在 `return { ... }` 中导出:`menuAll`, `menuCodes`, `fetchMenuCodes`。 + +- [ ] **Step 2: 确认 `AUTH_API` 已从 `@/config/api` 导入(已有),无需改 import 路径。** + +- [ ] **Step 3: Commit** + +```bash +git add miniprogram/stores/user.js +git commit -m "$(cat <<'EOF' +feat(miniprogram): load menu-codes into user store. + +Cache menuAll and menuCodes from /web/auth/menu-codes for workbench filtering. +EOF +)" +``` + +--- + +### Task 5: 工作台接入过滤 + +**Files:** +- Modify: `miniprogram/pages/workbench/workbench.vue` + +- [ ] **Step 1: 替换静态 modules 为 computed 过滤结果** + +将: + +```js +const modules = WORKBENCH_MODULES.filter((m) => m.visible !== false) +``` + +改为: + +```js +import { filterWorkbenchModules } from '@/utils/workbenchMenuFilter' + +const modules = computed(() => + filterWorkbenchModules(WORKBENCH_MODULES, { + menuAll: userStore.menuAll, + menuCodes: userStore.menuCodes + }) +) +``` + +(保留文件顶部已有的 `computed` import。) + +- [ ] **Step 2: `onShow` 中拉权限** + +```js +onShow(async () => { + userStore.checkLoginStatus() + if (!userStore.isLoggedIn) { + uni.reLaunch({ url: PAGE_PATHS.login }) + return + } + await userStore.fetchMenuCodes() +}) +``` + +- [ ] **Step 3: 手工验收** + +1. 超管登录工作台:见「地磅管理」四入口。 +2. Web「菜单管理」可见移动端 → 地磅管理树。 +3. 无 `mini:db:*` 的普通账号:工作台无地磅块。 +4. (可选)给普通角色只勾「计划管理」:仅显示计划入口。 + +- [ ] **Step 4: Commit** + +```bash +git add miniprogram/pages/workbench/workbench.vue +git commit -m "$(cat <<'EOF' +feat(miniprogram): filter workbench menus by mobile menu permissions. + +Hide weighbridge entries the current user is not granted via mini:db:* codes. +EOF +)" +``` + +--- + +## Spec 覆盖自检 + +| Spec 项 | Task | +|---------|------| +| SQL 插入 MINI_DB_* + mini:db:* | Task 1 | +| 仅超级管理员默认授权 | Task 1 | +| 复用 `/web/auth/menu-codes` | Task 3–4 | +| workbench-menus `perms` 映射 | Task 3 | +| store 缓存 + 工作台过滤 / 无叶子隐藏模块 | Task 2、4、5 | +| 幂等 SQL | Task 1 Step 2 | +| 非目标(API 鉴权、仪表盘裁剪、新 mini 接口) | 未列入任务 | + +--- + +## 执行交接 + +Plan complete and saved to `docs/superpowers/plans/2026-09-11-mini-db-menu-permission.md`. + +**两种执行方式:** + +1. **Subagent-Driven(推荐)** — 每任务派生子代理,任务间复核 +2. **Inline Execution** — 本会话按 executing-plans 连续执行并设检查点 + +选哪一种?