Skip to content

[imageUrl.ts]: 添加智能图片URL拼接功能#723

Open
bbwl521 wants to merge 107 commits into
mineadmin:masterfrom
bbwl521:feature/image-url-handler
Open

[imageUrl.ts]: 添加智能图片URL拼接功能#723
bbwl521 wants to merge 107 commits into
mineadmin:masterfrom
bbwl521:feature/image-url-handler

Conversation

@bbwl521
Copy link
Copy Markdown

@bbwl521 bbwl521 commented May 9, 2026

Summary

  • 新增 imageUrl.ts 工具函数,支持智能判断图片 URL 是否为完整路径
  • 如果 URL 是完整路径(带协议和域名),直接返回
  • 如果是相对路径,自动拼接 API 基础域名

修改的文件

  • web/src/utils/imageUrl.ts - 新增工具函数
  • web/src/components/ma-upload-image/index.vue - 图片上传组件
  • web/src/components/ma-resource-picker/panel.vue - 资源选择器组件
  • web/src/layouts/components/bars/toolbar/components/user-bar.tsx - 顶部用户栏
  • web/src/layouts/uc.tsx - 用户中心
  • web/src/modules/base/views/permission/user/data/getTableColumns.tsx - 用户列表
  • web/src/plugins/mine-admin/app-store/views/detail.vue - 应用商店详情
  • web/src/plugins/mine-admin/app-store/views/list.vue - 应用商店列表

Test plan

  • 测试上传图片后数据库存储相对路径时,图片能正常显示
  • 测试数据库存储完整 URL(带域名)时,图片能正常显示
  • 测试用户头像在不同场景下都能正常加载

Made with Cursor

Summary by CodeRabbit

发布说明

  • 新功能

    • 新增部门和岗位管理系统,支持创建、编辑和删除操作
    • 用户可以关联至部门和岗位
    • 实现数据权限控制系统,支持按部门、自创建人等多种权限范围
    • 新增部门领导管理功能
  • 文档

    • 更新README.md以澄清默认分支和功能说明

zds-s and others added 30 commits March 3, 2025 03:13
…and defaults; add PolicyType enum for data permission policies
…ce structure and enhance role/user associations
…epartment

# Conflicts:
#	app/Http/Admin/Controller/Permission/DepartmentController.php
* fix: 修正部门错别字

* feat: 补充岗位模块五大金刚文件
kanyxmo and others added 13 commits June 16, 2025 11:38
* fix(icons): 修复生成图标命令报缺少`inquirer`库

* fix(login): 修复用户登录后未设置语言标记,造成默认为英文的bug

* feat(plugin): 前端插件添加 loginBefore Hook,用于登录请求前处理提交的登录数据,可修改提交的登录数据到后端

* Update package.json (mineadmin#625)

* docs(README): Updated contributors graphs link (mineadmin#626)

* fix: 修复引用了Swow包的bug

* fix: 还原

* Fix/menu btn permissions (mineadmin#627)

* fix: 修复引用了Swow包的bug

* fix: 还原

* fix(menu-form): 修复菜单按钮权限删除,没有删除对应索引行问题

* fix(permission): 修复菜单保存,未更新删除btnPermission 数据项的问题

---------

Co-authored-by: X.Mo <[email protected]>

* fix: 适配最新ele版本的 el-link api

* fix: 适配最新ele版本的 el-link api

* fix: 适配最新ele版本的 el-link api

* update: 升级 @mineadmin/pro-table 至 1.0.73 和 @mineadmin/search 至 1.0.53,同时更新 element-plus 到 2.10.2

* Revert pull request (mineadmin#638)

* Update Server.php

文档地址无法打开了,更换最新文档地址

* Update docker-compose.yml

MineAdmin不符合docker-compose up的命名规范,改成小写字母。

* fix(package): 降低vue-i18n版本号

---------

Co-authored-by: 留恋风 <[email protected]>
Co-authored-by: west_ng <[email protected]>

* fix: update upload method parameter type hint to support Swow (mineadmin#640)

---------

Co-authored-by: X.Mo <[email protected]>
Co-authored-by: 暮秋人 <[email protected]>
Co-authored-by: 留恋风 <[email protected]>
Co-authored-by: west_ng <[email protected]>
- 在用户管理表单中添加删除数据权限开关
- 实现用户数据权限的删除逻辑- 更新相关语言文件,添加删除数据权限相关翻译
* optimize(User Policy): 优化删除用户数据权限UI 和 后端代码(整体形式改变)

* refactor(getFormItems): 移除删除策略字段的表单项

* fix(UserService): 修正更新用户策略时的代码缩进

* 更新 UserService.php

* 更新 UserService.php

* fix(UserService): 添加空值检查以防止创建空政策

* fix(UserService): 修正创建策略时的空值检查逻辑

* fix(DataScope): 添加清除功能以重置模型值
# Conflicts:
#	README-en.md
#	app/Http/CurrentUser.php
#	app/Service/Permission/UserService.php
#	web/src/components/ma-dialog/index.vue
#	web/src/store/modules/useUserStore.ts
#	web/types/components.d.ts
- 新增 imageUrl.ts 工具函数,支持判断完整URL和相对路径
- 修改 ma-upload-image 组件:预览和展示URL使用智能拼接
- 修改 ma-resource-picker 组件:封面和预览URL使用智能拼接
- 修改用户相关组件:头像URL使用智能拼接
- 修改应用商店组件:应用封面图使用智能拼接

Co-authored-by: Cursor <[email protected]>
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label May 9, 2026
@boring-cyborg
Copy link
Copy Markdown

boring-cyborg Bot commented May 9, 2026

Thanks for opening this pull request! Please check out our contributing guidelines. 感谢您开启此拉取请求!请查看我们的 贡献者指南

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

📝 Walkthrough

Walkthrough

新增组织(部门/岗位/领导)与数据权限体系;后端提供模型/仓储/服务/控制器与AOP数据范围;前端新增页面与API、字典和多语言;包含数据库迁移、配置与切面单测;统一图片URL与组件细节。

Changes

组织与数据权限一体化改造

Layer / File(s) Summary
数据契约
app/Http/Admin/Request/*, app/Schema/*
新增/更新请求校验与Swagger Schema,覆盖部门/岗位/领导/用户/角色及策略。
模型与迁移
app/Model/..., databases/migrations/*, databases/seeders/*
引入Department/Position/Leader/Policy与枚举,扩展User关系;创建/更新相关表与菜单种子。
数据权限内核
app/Library/DataPermission/**
加入ScopeType、DataScope注解/上下文、AOP切面与全局Scope,规则工厂与执行器、缓存与自定义函数。
仓储与服务
app/Repository/**, app/Service/**, app/Http/CurrentUser.php
实现检索与事务化创建更新、关联同步与策略维护;CurrentUser支持Context缓存。
控制器与路由
app/Http/Admin/Controller/**
提供部门/岗位/领导的分页、增改删与岗位数据范围设置。
配置与测试
config/**, phpstan.neon.dist, tests/**
新增数据权限缓存/自定义规则与调试开关,移除Casbin模型;添加切面单测。
前端基础
web/src/utils/*, web/src/provider/dictionary/*, web/src/locales/*, web/types/*
统一图片URL、完善下载注释;新增数据范围字典与多语言,补充全局组件类型。
前端API
web/src/modules/base/api/*
新增部门/岗位/领导API与用户类型扩展。
前端页面/组件
web/src/modules/base/views/**, web/src/components/**, web/src/layouts/**, web/src/plugins/**
新增组织管理与数据范围配置页面,用户按部门筛选;组件与插件页统一图片URL。
文档
README.md
更新默认分支与内置功能说明。

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement, Frontend, size:M

Suggested reviewers

  • kanyxmo
  • People-Sea

Poem

小兔举灯照枝端,
部门成林岗成山。
数据设篱风不乱,
范围随步影相伴。
前后同拍一声赞,
春芽点点映屏间。 🥕

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@dosubot dosubot Bot added the enhancement label May 9, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
app/Library/DataPermission/ScopeType.php (1)

1-29: ⚠️ Potential issue | 🟠 Major

PR 内容与目标不一致。

PR 标题和目标描述的是"添加智能图片URL拼接功能",涉及 imageUrl.ts 工具函数和图片展示组件。但实际审查的文件都与数据权限系统相关(ScopeType 枚举、dataScope 本地化翻译、用户策略字段等),与图片 URL 处理无关。

请确认:

  1. 是否提供了正确的文件进行审查?
  2. PR 目标描述是否准确?
  3. 是否在一个 PR 中混合了多个不相关的功能?

建议将不相关的功能拆分到独立的 PR 中,以便更好地追踪和审查变更。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/ScopeType.php` around lines 1 - 29, The PR mixes
unrelated changes: your diff touches App\Library\DataPermission\ScopeType (the
ScopeType enum) and other data-permission assets while the PR title/description
mention adding imageUrl.ts and image display changes; update the PR so files
align with the described feature by either (A) correcting the branch/commit to
include the actual image URL utility and UI component changes (referencing
imageUrl.ts and the image display component) or (B) split out the
data-permission changes (ScopeType enum and any dataScope/localization/user
policy changes) into a separate PR; ensure the PR description, title, and
changed files consistently reflect the intended scope before requesting review.
web/src/modules/base/locales/en[English].yaml (1)

127-151: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

i18n 层级被意外改变,log / dataCenter 键路径会失效。

basePost 插入在这里后,后续 logdataCenter 变成了 basePost.logbasePost.dataCenter,会影响原有 baseMenu.* 读取路径。

🔧 建议修复(调整块顺序,保持 baseMenu 原层级)
 baseMenu:
   permission:
     ...
- basePost:
-  name: Post Name
-  belongPost: Belongs to Position
-  belongDept: Belongs to Department
-  created_at: Creation Time
-  updated_at: Update Time
-  dataScope: Data Permissions
-  selectDept: Selected Departments
-  callFunc: Function Call Name
-  placeholder:
-    name: Please enter position name
   log:
     index: Log Management
     ...
   dataCenter:
     index: Data Center
     ...
+basePost:
+  name: Post Name
+  belongPost: Belongs to Position
+  belongDept: Belongs to Department
+  created_at: Creation Time
+  updated_at: Update Time
+  dataScope: Data Permissions
+  selectDept: Selected Departments
+  callFunc: Function Call Name
+  placeholder:
+    name: Please enter position name
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/locales/en`[English].yaml around lines 127 - 151, The
YAML insertion of basePost changed the hierarchy so that log and dataCenter are
now nested under basePost; move the entire basePost mapping so that log and
dataCenter remain siblings of baseMenu (not children of basePost) — locate the
basePost block and adjust its position in the file to restore log and dataCenter
at the original top-level (same level as baseMenu) so keys like baseMenu.log and
baseMenu.dataCenter continue to resolve correctly.
web/src/modules/base/locales/zh_TW[繁體中文].yaml (1)

127-151: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

basePost 插入位置导致 log / dataCenter 鍵層級漂移。

目前結構會把原本屬於 baseMenu 的日誌與數據中心鍵歸到 basePost 下,會造成既有 i18n 路徑失效。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/locales/zh_TW`[繁體中文].yaml around lines 127 - 151, The
YAML shows that log and dataCenter mappings are indented under basePost causing
key-level drift; move the log: and dataCenter: blocks out of the basePost
mapping so they become siblings of basePost (i.e., at the same indentation level
as basePost) and restore them under their intended parent (likely baseMenu);
update the entries referencing basePost, baseMenu, log, and dataCenter to ensure
i18n paths match the original keys.
web/src/components/ma-dialog/index.vue (1)

108-113: ⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

全屏切换按钮失去了交互功能

在 Line 108 移除了 @click="() => fullscreen = !fullscreen" 处理器后,全屏切换按钮变成了一个无法交互的显示元素。虽然图标仍然会根据 fullscreen 状态变化,但用户点击按钮时不会有任何响应,这可能导致用户困惑。

建议:

  1. 如果全屏功能需要从外部通过 v-model:fullscreen 控制,应考虑移除此按钮或将其改为纯展示状态
  2. 如果需要保留用户点击切换功能,应恢复点击处理器
🔄 恢复点击交互功能的建议修复
-        <el-link class="el-dialog__headerbtn relative !right-[2px] !-top-[6px]" underline="never">
+        <el-link class="el-dialog__headerbtn relative !right-[2px] !-top-[6px]" underline="never" `@click`="() => fullscreen = !fullscreen">
           <ma-svg-icon
             :name="fullscreen ? fsIcon.exit : fsIcon.todo"
             :size="15"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/ma-dialog/index.vue` around lines 108 - 113, The
fullscreen icon button lost interactivity because the click handler was removed;
restore click behavior on the el-link containing ma-svg-icon: if fullscreen is a
local reactive data property (used in this component) re-add a click handler
that toggles fullscreen (e.g., set fullscreen = !fullscreen), otherwise if
fullscreen is provided via v-model:fullscreen treat it as a prop and emit an
update event instead (emit('update:fullscreen', !fullscreen)); update the
el-link to handle keyboard accessibility (role/button or tabindex) as
appropriate and ensure fsIcon and ma-svg-icon remain unchanged.
🟠 Major comments (26)
web/src/utils/download.ts-50-59 (1)

50-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

缺少 base64 解码的错误处理

base64 方法在解码时缺少错误处理:

  • 第 51 行:如果 base64Str 不包含逗号,data 将是 undefined
  • 第 53 行:atob(data) 在接收无效 base64 字符串时会抛出异常

这会导致运行时错误,影响用户体验。

🛡️ 建议的修复方案
   base64: (base64Str: string, fileName: string) => {
+    if (!base64Str || !base64Str.includes(',')) {
+      console.error('Invalid base64 string format')
+      return
+    }
     const [header, data] = base64Str.split(',')
     const mime = header?.match(/:(.*?);/)?.[1] || 'application/octet-stream'
+    try {
       const binary = atob(data)
       const u8arr = new Uint8Array(binary.length)
       for (let i = 0; i < binary.length; i++) {
         u8arr[i] = binary.charCodeAt(i)
       }
       const blob = new Blob([u8arr], { type: mime })
       handleBlob(blob, fileName, mime)
+    } catch (error) {
+      console.error('Failed to decode base64 string:', error)
+    }
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/utils/download.ts` around lines 50 - 59, The base64 function can
crash when input is malformed (missing comma or invalid base64); fix it by
validating base64Str before decoding and wrapping the atob step in a try/catch:
check base64Str.includes(',') and that data is a non-empty string, attempt
decoding inside try, and on error either log/return a graceful failure or
surface a clear error message instead of letting the exception propagate; update
the base64 method (referenced as base64) to perform these checks and error
handling before creating the Uint8Array and calling handleBlob(blob, fileName,
mime).
web/src/modules/base/api/user.ts-28-30 (1)

28-30: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

改进类型定义以提高类型安全性。

新增字段使用 any 类型降低了 TypeScript 的类型安全性。建议为 policydepartmentposition 定义明确的接口类型。

♻️ 建议的类型改进
+export interface PolicyVo {
+  // 根据实际策略结构定义字段
+  type?: string
+  scope?: string
+  // ... 其他字段
+}
+
+export interface DepartmentVo {
+  id?: number
+  name?: string
+  // ... 其他字段
+}
+
+export interface PositionVo {
+  id?: number
+  name?: string
+  // ... 其他字段
+}
+
 export interface UserVo {
   id?: number
   username?: string
   // ... 其他字段
-  policy?: any
-  department?: any[]
-  position?: any[]
+  policy?: PolicyVo
+  department?: DepartmentVo[]
+  position?: PositionVo[]
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/api/user.ts` around lines 28 - 30, The current loose
types policy?: any, department?: any[], and position?: any[] reduce TypeScript
safety; replace them with explicit types (e.g., policy?: Policy, department?:
Department[], position?: Position[]) by declaring or importing appropriate
interfaces/types (Policy, Department, Position) and update any code that
constructs or consumes these properties to conform to the new shapes; ensure the
definitions include required fields used elsewhere (IDs, names, permissions,
etc.) and run the TS compiler to fix any resulting type errors.
app/Library/DataPermission/Rule/CustomFuncFactory.php-22-25 (1)

22-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

同名函数静默覆盖会导致数据权限规则不可预期。

当前重复注册同名 custom func 会直接覆盖旧实现,建议显式拒绝重复注册(或提供显式 replace 参数),避免权限策略被无感改写。

🔧 建议修复
 public static function registerCustomFunc(string $name, \Closure $func): void
 {
+    if (isset(self::$customFunc[$name])) {
+        throw new \RuntimeException(sprintf('Custom func "%s" already registered', $name));
+    }
     self::$customFunc[$name] = $func;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Rule/CustomFuncFactory.php` around lines 22 - 25,
registerCustomFunc 目前会无条件覆盖同名函数,应改为显式拒绝重复注册或提供可选替换参数以避免无感改写权限策略;在
CustomFuncFactory::registerCustomFunc 检查 self::$customFunc[$name]
是否已存在,若存在则抛出异常(例如 InvalidArgumentException)或新增第三个参数 bool $replace = false 并在
$replace 为 true 时才替换已注册的闭包;确保抛出的异常或返回值有明确信息并在文档注释中说明行为。
web/src/modules/base/views/permission/department/data/getFormItems.tsx-22-25 (1)

22-25: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

缺少请求失败处理,异常时表单会静默降级。

部门列表请求只有成功分支,建议补充 catch 并给出提示/兜底数据,避免用户无感失败。

🔧 建议修复
 useHttp().get('/admin/department/list?level=1').then((res: any) => {
   deptList.value = res.data.list
   deptList.value.unshift({ id: 0, name: '顶级部门', value: 0 } as any)
-})
+}).catch(() => {
+  deptList.value = [{ id: 0, name: '顶级部门', value: 0 } as any]
+  msg.error(t('baseDepartment.error.loadFailed'))
+})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/department/data/getFormItems.tsx`
around lines 22 - 25, The department list request uses useHttp().get(...) with
only a .then branch so failures silently degrade; update the call in
getFormItems (the block that sets deptList.value) to add a .catch handler that
logs or shows a user-facing error (e.g., via your notification/logger), and sets
a safe fallback for deptList.value (at minimum the top-level item { id: 0, name:
'顶级部门', value: 0 }) to avoid an empty form. Keep the existing success logic in
the .then and optionally add a .finally if you need cleanup.
databases/seeders/user_dept_20250310.php-24-86 (1)

24-86: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Seeder 非幂等,重复执行会产生重复权限菜单。

这里全量使用 create,二次执行会重复插入同名菜单。建议改为 updateOrCreate(至少以 name 作为唯一条件)保证可重复执行安全。

🔧 建议修复(示意)
-$now = Menu::create([
+$now = Menu::query()->updateOrCreate(
+    ['name' => 'permission:department'],
+    [
       'name'  =>  'permission:department',
       'path'  =>  '/permission/departments',
       'parent_id' => $parent->id,
       'component' => 'base/views/permission/department/index',
       'meta'  =>  [
         ...
       ]
-]);
+    ]
+);

 foreach ($children as $child => $title) {
-    Menu::create([
-        'name'  =>  $child,
-        'path'  =>  '/permission/departments',
-        'meta'  =>  [
-            ...
-        ],
-        'parent_id' => $now->id
-    ]);
+    Menu::query()->updateOrCreate(
+        ['name' => $child],
+        [
+            'path' => '/permission/departments',
+            'meta' => [
+                ...
+            ],
+            'parent_id' => $now->id,
+        ]
+    );
     $i++;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@databases/seeders/user_dept_20250310.php` around lines 24 - 86, The seeder is
not idempotent because it uses Menu::create for the parent ($now) and for each
child in the $children loop; replace those calls with Menu::updateOrCreate keyed
by the unique 'name' (use ['name' => $child] for children and ['name' =>
'permission:department'] for the parent) and pass the meta/parent_id attributes
as the second argument so re-running the seeder updates existing records instead
of inserting duplicates; ensure you still set $now to the resolved parent record
before creating/updating children and keep the $i18n mapping logic intact.
databases/migrations/2025_02_24_195620_create_department_tables.php-36-56 (1)

36-56: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

关联表缺少主键或唯一约束,存在数据完整性风险。

user_deptuser_positiondept_leader 表缺少主键或唯一约束,这将允许插入重复记录:

  • 同一用户可以被多次关联到同一部门/岗位
  • 同一用户可以被多次设置为同一部门的领导

这可能导致数据冗余和业务逻辑错误。

🛡️ 建议的修复

为关联表添加复合主键或唯一约束:

 Schema::create('user_dept', function (Blueprint $table) {
     $table->comment('用户-部门关联表');
     $table->bigInteger('user_id');
     $table->bigInteger('dept_id');
+    $table->primary(['user_id', 'dept_id']);
     $table->datetimes();
     $table->softDeletes();
 });
 
 Schema::create('user_position', function (Blueprint $table) {
     $table->comment('用户-岗位关联表');
     $table->bigInteger('user_id');
     $table->bigInteger('position_id');
+    $table->primary(['user_id', 'position_id']);
     $table->datetimes();
     $table->softDeletes();
 });
 
 Schema::create('dept_leader', function (Blueprint $table) {
     $table->comment('部门领导表');
     $table->bigInteger('dept_id');
     $table->bigInteger('user_id');
+    $table->primary(['dept_id', 'user_id']);
     $table->datetimes();
     $table->softDeletes();
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@databases/migrations/2025_02_24_195620_create_department_tables.php` around
lines 36 - 56, The three association tables (user_dept, user_position,
dept_leader) created in the Schema::create blocks lack primary keys or unique
constraints; update each Blueprint ($table) to enforce uniqueness by adding a
composite primary key or unique index on the related columns (e.g. for user_dept
use a composite constraint on ['user_id','dept_id'], for user_position on
['user_id','position_id'], and for dept_leader on ['dept_id','user_id']) using
$table->primary(...) or $table->unique(...); ensure you modify the
Schema::create closures for user_dept, user_position and dept_leader to include
these constraints so duplicate associations cannot be inserted.
databases/migrations/2025_02_24_195620_create_department_tables.php-20-67 (1)

20-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

建议为外键列添加索引以提升查询性能。

多个表中的外键列缺少索引,这可能导致关联查询时的性能问题:

  • department.parent_id (Line 24)
  • position.dept_id (Line 32)
  • data_permission_policy.user_id (Line 60)
  • data_permission_policy.position_id (Line 61)
  • user_dept.user_iduser_dept.dept_id (Lines 38-39)
  • user_position.user_iduser_position.position_id (Lines 45-46)
  • dept_leader.dept_iddept_leader.user_id (Lines 52-53)

随着数据量增长,缺少索引会导致全表扫描,严重影响查询效率。

🔍 建议的索引添加

在相应的表创建语句中添加索引:

 Schema::create('department', function (Blueprint $table) {
     $table->comment('部门表');
     $table->bigIncrements('id');
     $table->string('name', 50)->comment('部门名称');
     $table->bigInteger('parent_id')->default(0)->comment('父级部门ID');
+    $table->index('parent_id');
     $table->datetimes();
     $table->softDeletes();
 });
 
 Schema::create('position', function (Blueprint $table) {
     $table->comment('岗位表');
     $table->bigIncrements('id');
     $table->string('name', 50)->comment('岗位名称');
     $table->bigInteger('dept_id')->comment('部门ID');
+    $table->index('dept_id');
     $table->datetimes();
     $table->softDeletes();
 });
 
 Schema::create('user_dept', function (Blueprint $table) {
     $table->comment('用户-部门关联表');
     $table->bigInteger('user_id');
     $table->bigInteger('dept_id');
+    $table->index('user_id');
+    $table->index('dept_id');
     $table->datetimes();
     $table->softDeletes();
 });

对其他表应用类似的索引添加。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@databases/migrations/2025_02_24_195620_create_department_tables.php` around
lines 20 - 67, Migration creates several FK-like columns without indexes causing
potential performance issues; update the migration to add indexes for the listed
columns (e.g., in the Schema::create blocks for department, position, user_dept,
user_position, dept_leader, data_permission_policy) by adding index definitions
for department.parent_id, position.dept_id, data_permission_policy.user_id,
data_permission_policy.position_id, user_dept.user_id, user_dept.dept_id,
user_position.user_id, user_position.position_id, and dept_leader.dept_id and
dept_leader.user_id (use $table->index(...) or $table->bigInteger(...)->index()
or $table->index(['col1','col2']) where appropriate); run migrations or refresh
tests to verify indexes are created.
app/Schema/UserSchema.php-75-77 (1)

75-77: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

policy 已建模但未序列化输出,接口契约不一致。

当前 UserSchema 已在 Line 75 定义并在 Line 97 赋值 policy,但 jsonSerialize() 不返回该字段,前端将拿不到该数据。

建议修复
 public function jsonSerialize(): mixed
 {
-    return ['id' => $this->id, 'username' => $this->username, 'user_type' => $this->userType, 'nickname' => $this->nickname, 'phone' => $this->phone, 'email' => $this->email, 'avatar' => $this->avatar, 'signed' => $this->signed, 'status' => $this->status, 'login_ip' => $this->loginIp, 'login_time' => $this->loginTime, 'backend_setting' => $this->backendSetting, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark];
+    return ['id' => $this->id, 'username' => $this->username, 'user_type' => $this->userType, 'nickname' => $this->nickname, 'phone' => $this->phone, 'email' => $this->email, 'avatar' => $this->avatar, 'signed' => $this->signed, 'status' => $this->status, 'login_ip' => $this->loginIp, 'login_time' => $this->loginTime, 'backend_setting' => $this->backendSetting, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy?->jsonSerialize()];
 }

Also applies to: 97-97

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Schema/UserSchema.php` around lines 75 - 77, UserSchema defines a public
?PolicySchema $policy (annotated Property 'policy') and it's assigned elsewhere
but not included in the jsonSerialize() output; update the
UserSchema::jsonSerialize method to include the 'policy' key (with null-safe
handling) in the returned array so the serialized contract matches the
schema/assignment, ensuring the field is present when set and omitted or
null-consistent when unset.
web/src/modules/base/views/permission/department/viewUser.vue-25-30 (1)

25-30: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

表格数据对 props.data 变更不响应,可能显示旧数据。

options.data 在初始化时就固定了;当父组件后续更新 data.department_users 时,这里不会自动刷新。

建议修复(使用计算属性)
-const options = ref<MaTableOptions>({
-  data: data?.department_users ?? [],
-  adaption: false,
-  height: 400,
-  rowKey: 'id',
-})
+const options = computed<MaTableOptions>(() => ({
+  data: data?.department_users ?? [],
+  adaption: false,
+  height: 400,
+  rowKey: 'id',
+}))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/department/viewUser.vue` around lines
25 - 30, options.data is initialized once so it won't react to updates of
props.data (data.department_users); change it to be reactive by deriving the
table rows from a computed or by updating options.value.data in a watcher:
create a computed (e.g., usersList = computed(() => data?.department_users ??
[])) and use that instead of the static array in MaTableOptions, or add a watch
on data.department_users to set options.value.data = newValue; update references
to options and MaTableOptions accordingly so the table always reflects changes
to data.department_users.
app/Http/Admin/Request/Permission/RoleRequest.php-23-23 (1)

23-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

新增 policy 仅更新文档,未加入请求校验。

目前只在注解中开放了 policy,但 rules() 没有对应规则,权限策略字段会缺少输入约束,存在数据不一致风险。

建议补充(示例)
 public function rules(): array
 {
     $rules = [
         'name' => 'required|string|max:60',
         'code' => [
             'required',
             'string',
             'max:60',
             'regex:/^[a-zA-Z0-9_]+$/',
         ],
         'status' => 'sometimes|integer|in:1,2',
         'sort' => 'required|integer',
         'remark' => 'nullable|string|max:255',
+        'policy' => 'nullable|array',
+        'policy.depts' => 'sometimes|array',
+        'policy.roles' => 'sometimes|array',
     ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Admin/Request/Permission/RoleRequest.php` at line 23, The
RoleRequest class exposes a new field 'policy' in annotations but the rules()
method lacks validation for it; update the RoleRequest::rules() to include a
validation rule for 'policy' (e.g. add 'policy' => ['nullable','string'] or if
it is structured use 'policy' => ['nullable','array'] plus per-item rules like
'policy.*' => ['string','max:...']) so the request enforces the correct
type/constraints and prevents inconsistent data; pick the variant matching how
'policy' is stored and apply it inside rules().
app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php-64-119 (1)

64-119: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

严重 N+1:多层嵌套 each 内部反复 whereHas('department')->get() 会爆 SQL 数量。

每个 Department / Position 都会单独发起一次 User::whereHas('department', ...)->get(),而 getFlatChildren() 又会展开整棵子树。一个用户隶属多个部门、且部门子节点较多时,执行次数是 O(部门 × 子部门 × 岗位),稍具规模的组织就会显著拖慢请求,甚至打爆数据库连接。

建议先把所有目标 department_id 聚合成一个数组,再用一次 User::query()->whereHas('department', fn ($q) => $q->whereIn('id', $deptIds))->pluck('id')->all() 取出所有用户 id 合并到 $ids

⚡ 思路示例
$deptIds = collect();

// 1) 当前用户自身的部门 / 部门树
$ownDepts = $this->getUser()->department()->get();
foreach ($ownDepts as $dept) {
    $deptIds = $policyType->isDeptTree()
        ? $deptIds->merge($dept->getFlatChildren()->pluck('id'))
        : $deptIds->push($dept->id);
}

// 2) 当前用户岗位下的部门 / 部门树
foreach ($this->getUser()->position()->get() as $position) {
    foreach ($position->department()->get() as $dept) {
        $deptIds = $policyType->isDeptTree()
            ? $deptIds->merge($dept->getFlatChildren()->pluck('id'))
            : $deptIds->push($dept->id);
    }
}

// 3) 策略显式指定的部门集合(CustomDept / CustomFunc)也合并进来
foreach ($departmentList as $dept) {
    $deptIds = $policyType->isDeptTree()
        ? $deptIds->merge($dept->getFlatChildren()->pluck('id'))
        : $deptIds->push($dept->id);
}

$deptIds = $deptIds->unique()->values()->all();

if (!empty($deptIds)) {
    $userIds = User::query()
        ->whereHas('department', fn ($q) => $q->whereIn('id', $deptIds))
        ->pluck('id')
        ->all();
    $ids = array_merge($ids, $userIds);
}

return array_values(array_unique($ids));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php` around
lines 64 - 119, The current nested each loops in CreatedByIdsExecute (using
getUser()->newQuery()->whereHas('department')->get() inside
getFlatChildren()/position()->department()->each) cause an N+1 SQL explosion;
instead, collect all target department IDs first (use
getFlatChildren()->pluck('id') when policyType->isDeptTree(), include department
IDs from leaderDepartmentList, each position()->department(), and the user's own
department via getUser()->department()), deduplicate them, then perform a single
User query (e.g. User::query()->whereHas('department', fn($q) =>
$q->whereIn('id', $deptIds))->pluck('id')) and merge those IDs into $ids; ensure
you replace the multiple whereHas()->get() calls with this single bulk lookup
and return unique user IDs.
web/src/modules/base/views/permission/user/form.vue-71-73 (1)

71-73: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

policy 为空时被赋值为数组,数据结构不正确

Line 72 将 policy 设为 [] 会破坏后续对象字段访问和后端入参结构(例如 policy_type/value)。这里应初始化为对象结构。

🔧 建议修改
-    if (userModel.value.policy === null) {
-      userModel.value.policy = []
-    }
+    if (userModel.value.policy === null) {
+      userModel.value.policy = {
+        policy_type: '',
+        value: [],
+      } as any
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/user/form.vue` around lines 71 - 73,
The code sets userModel.value.policy = [] when null, which breaks expected
object access and backend payloads; change the initialization in the block that
checks userModel.value.policy to assign an object (e.g., an empty object or the
expected shape containing keys like policy_type and value) instead of an array
so subsequent code accessing userModel.value.policy.policy_type/value and
request serialization see the correct structure.
web/src/modules/base/views/permission/department/position.vue-229-233 (1)

229-233: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

新增操作未重置 postModel,可能提交脏数据

Line 230 只清空了 name。如果此前点过编辑/数据权限,idpolicy_typevalue 等字段会残留到新增请求里,存在错误写入风险。

🔧 建议修改
 `@click`="() => {
-  postModel.name = ''
+  Object.assign(postModel, {
+    id: undefined,
+    name: '',
+    policy_type: '',
+    value: [],
+    func_name: undefined,
+    dept_id: data?.id,
+    dept_name: data?.name,
+  })
   maDialog.setTitle(t('crud.add'))
   maDialog.open({ formType: 'add' })
 }"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/department/position.vue` around lines
229 - 233, The add-button handler only clears postModel.name which can leave id,
policy_type, value, etc. from prior edits and cause dirty submissions; update
the click handler so it resets the entire postModel to its default empty state
(e.g. replace postModel with a fresh default object or assign default values)
before calling maDialog.setTitle(...) and maDialog.open({ formType: 'add' }) to
ensure no stale fields are submitted.
web/src/modules/base/views/permission/department/position.vue-54-94 (1)

54-94: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

okLoadingState(false) 触发时机过早,导致加载态失真

Line 93 在异步请求未完成时就关闭 loading,用户会看到按钮可重复点击,且状态与真实请求不同步。建议把关闭 loading 放到 finally 里。

🔧 建议修改
-  ok: ({ formType }, okLoadingState: (state: boolean) => void) => {
-    okLoadingState(true)
-    if (['add', 'edit'].includes(formType)) {
-      const elForm = positionForm.value?.getElFormRef()
-      elForm?.validate?.().then(() => {
-        ...
-      }).catch()
-    }
-    else if (formType === 'setDataScope') {
-      ...
-    }
-    okLoadingState(false)
-  },
+  ok: async ({ formType }, okLoadingState: (state: boolean) => void) => {
+    okLoadingState(true)
+    try {
+      if (['add', 'edit'].includes(formType)) {
+        const elForm = positionForm.value?.getElFormRef()
+        await elForm?.validate?.()
+        // add/edit 分支改为 await create/save
+      }
+      else if (formType === 'setDataScope') {
+        // setDataScope 分支改为 await setDataScope
+      }
+    }
+    finally {
+      okLoadingState(false)
+    }
+  },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/department/position.vue` around lines
54 - 94, The ok handler currently calls okLoadingState(false) immediately after
starting async flows, causing loading to stop before requests finish; move the
call into each promise/finally so loading is cleared only after completion
(success or error). Specifically, in the ok: ({ formType }, okLoadingState) => {
... } function, remove the premature okLoadingState(false) and ensure
create(postModel.value).then(...).finally(() => okLoadingState(false)),
save(...).then(...).finally(() => okLoadingState(false)), and
setDataScope(...).then(...).finally(() => okLoadingState(false)) (or wrap the
validate()→switch block in a Promise chain with a single finally) so every async
branch (create, save, setDataScope and the elForm.validate flow) always calls
okLoadingState(false) in finally.
web/src/modules/base/views/permission/user/form.vue-36-42 (1)

36-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

未判空直接访问 policy.policy_type,有运行时异常风险

Line 37 在 policy 为空时会抛错。建议先拿本地变量并判空后再处理 CUSTOM_FUNC/CUSTOM_DEPT

🔧 建议修改
   ok: () => {
-    if (userModel.value.policy.policy_type === 'CUSTOM_FUNC') {
-      userModel.value.policy.value = [userModel.value.policy.func_name]
+    const policy = userModel.value.policy
+    if (!policy) {
+      maDialog.close()
+      return
+    }
+    if (policy.policy_type === 'CUSTOM_FUNC') {
+      policy.value = [policy.func_name]
     }
-    if (userModel.value.policy.policy_type === 'CUSTOM_DEPT') {
-      userModel.value.policy.value = scopeRef.value.deptRef.elTree?.getCheckedKeys()
+    if (policy.policy_type === 'CUSTOM_DEPT') {
+      policy.value = scopeRef.value.deptRef.elTree?.getCheckedKeys()
     }
     maDialog.close()
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/user/form.vue` around lines 36 - 42,
The code accesses userModel.value.policy.policy_type without null checks which
can throw if policy is null/undefined; update the ok handler to first read const
policy = userModel.value.policy and guard: if (!policy) return (or set a
default) before checking policy.policy_type, then handle the CUSTOM_FUNC branch
using policy.func_name and set policy.value, and handle CUSTOM_DEPT by safely
referencing scopeRef.value?.deptRef?.elTree?.getCheckedKeys() to avoid runtime
errors; ensure you update the same symbols (ok, userModel, policy, policy_type,
func_name, value, scopeRef, deptRef, elTree, getCheckedKeys) so behavior remains
identical when policy exists.
app/Model/Permission/Position.php-47-47 (1)

47-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

收敛 fillable,避免系统字段被批量赋值。

id、时间戳和 deleted_at 放进可批量赋值字段,会放大脏数据与越权写入风险。建议只保留业务可写字段。

🔧 建议修改
-    protected array $fillable = ['id', 'name', 'dept_id', 'created_at', 'updated_at', 'deleted_at'];
+    protected array $fillable = ['name', 'dept_id'];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Model/Permission/Position.php` at line 47, The Position model's protected
array $fillable currently exposes system fields; remove 'id', 'created_at',
'updated_at', and 'deleted_at' from $fillable and restrict it to only
business-writable fields (e.g., 'name' and 'dept_id') in the Position class so
system timestamps and primary key cannot be mass-assigned.
web/src/modules/base/views/permission/department/setLeader.vue-209-211 (1)

209-211: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

搜索提交对 form.users 未判空,空条件搜索会抛异常。

Line [210] 直接取 form.users.id,在未选择用户时会触发运行时错误。

🔧 建议修改
-    `@search-submit`="(form: any) => {
-      form.user_id = form.users.id
-    }"
+    `@search-submit`="(form: any) => {
+      form.user_id = form.users?.id
+    }"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/department/setLeader.vue` around lines
209 - 211, The inline `@search-submit` handler currently accesses form.users.id
without null checks and throws when no user is selected; update the handler for
the search-submit event so it first checks whether form.users is truthy (and
optionally form.users.id exists) before assigning form.user_id — if no user is
selected set form.user_id to null/undefined (or delete the key) to represent an
empty search; locate the anonymous handler where you see "(form: any) => {
form.user_id = form.users.id }" and add the conditional guard around that
assignment.
app/Http/Admin/Request/Permission/DepartmentRequest.php-40-47 (1)

40-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

补上 parent_id 的合法性约束,避免部门树脏数据。

parent_id 目前只校验整数;在更新场景还可能把父级设为自身。建议在 Line [40] 与更新分支同时增加 existsnot_in 约束,避免孤儿节点/自循环。

🔧 建议修改
-            'parent_id' => 'sometimes|integer',
+            'parent_id' => 'sometimes|integer|exists:department,id',
         ];
@@
         if ($this->isUpdate()) {
             $rules['name'] = 'required|string|max:60|unique:department,name,' . $this->route('id');
+            $rules['parent_id'] = 'sometimes|integer|exists:department,id|not_in:' . $this->route('id');
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Admin/Request/Permission/DepartmentRequest.php` around lines 40 -
47, The parent_id rule in DepartmentRequest currently only checks integer;
update the validation to also ensure the parent exists and is not the department
itself by adding an 'exists:department,id' constraint and, in the isUpdate()
branch, a 'not_in:'.$this->route('id') constraint for parent_id (use the same
$rules array and the existing route('id') value) so that parent_id is validated
for existence and prevented from referencing self during updates.
web/src/modules/base/views/permission/department/setLeader.vue-52-85 (1)

52-85: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ok 提交流程的异步控制有缺陷,导致失败也关闭弹窗且 loading 过早结束。

当前实现中 okLoadingState(false) 会在异步请求完成前执行,且 create/save 返回失败码时仍会执行 close/refresh。建议改为 try/finally,并仅在成功码时关闭与刷新。

🔧 建议修改
 const maDialog: UseDialogExpose = useDialog({
   lgWidth: '550px',
-  ok: ({ formType }, okLoadingState: (state: boolean) => void) => {
+  ok: async ({ formType }, okLoadingState: (state: boolean) => void) => {
+    if (!['add', 'edit'].includes(formType)) return
     okLoadingState(true)
-    if (['add', 'edit'].includes(formType)) {
-      const elForm = leaderForm.value?.getElFormRef()
-      // 验证通过后
-      elForm?.validate?.().then(() => {
-        leaderModel.value.user_id = leaderModel.value.users.map((item: any) => item.id)
-        delete leaderModel.value.users
-        switch (formType) {
-          // 新增
-          case 'add':
-            create(leaderModel.value).then((res: any) => {
-              res.code === ResultCode.SUCCESS ? msg.success(t('crud.createSuccess')) : msg.error(res.message)
-              maDialog.close()
-              proTableRef.value?.refresh()
-            }).catch((err: any) => {
-              msg.alertError(err)
-            })
-            break
-          // 修改
-          case 'edit':
-            save(leaderModel.value?.id as number, leaderModel.value).then((res: any) => {
-              res.code === 200 ? msg.success(t('crud.updateSuccess')) : msg.error(res.message)
-              maDialog.close()
-              proTableRef.value?.refresh()
-            }).catch((err: any) => {
-              msg.alertError(err)
-            })
-            break
-        }
-      }).catch()
-    }
-    okLoadingState(false)
+    try {
+      const elForm = leaderForm.value?.getElFormRef()
+      await elForm?.validate?.()
+      leaderModel.value.user_id = leaderModel.value.users.map((item: any) => item.id)
+      delete leaderModel.value.users
+
+      const res = formType === 'add'
+        ? await create(leaderModel.value)
+        : await save(leaderModel.value?.id as number, leaderModel.value)
+
+      if (res.code !== ResultCode.SUCCESS) {
+        msg.error(res.message)
+        return
+      }
+      msg.success(formType === 'add' ? t('crud.createSuccess') : t('crud.updateSuccess'))
+      maDialog.close()
+      proTableRef.value?.refresh()
+    }
+    catch (err: any) {
+      msg.alertError(err)
+    }
+    finally {
+      okLoadingState(false)
+    }
   },
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/department/setLeader.vue` around lines
52 - 85, The ok handler's async flow prematurely calls okLoadingState(false) and
closes the dialog/refreshes on non-success responses; refactor the ok function
(the ok: ({ formType }, okLoadingState) => { ... }) to await the form validation
and then perform the create/save calls inside a try/catch/finally: set
okLoadingState(true) before awaiting, in the try await create(...) or save(...),
only call maDialog.close() and proTableRef.value?.refresh() when the response
indicates success (check res.code === ResultCode.SUCCESS or res.code === 200),
handle errors in catch (msg.alertError), and in finally call
okLoadingState(false) so loading is always cleared after the async work;
reference leaderForm.value?.getElFormRef(), leaderModel.value, create, save,
maDialog.close, proTableRef.value?.refresh, and okLoadingState in your changes.
app/Library/DataPermission/Rule/Executable/AbstractExecutable.php-58-64 (1)

58-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

$policy->value[0] 缺少防御式校验,异常策略会直接打断执行。

在自定义函数执行前应先校验策略值结构,避免未定义下标或非法函数标识导致运行时错误。

🔧 建议修改
     protected function loadCustomFunc(Policy $policy): Collection
     {
-        $result = CustomFuncFactory::getCustomFunc($policy->value[0])->call($this, $this->getUser(), $this->getPolicy());
+        $funcName = $policy->value[0] ?? null;
+        if (! is_string($funcName) || $funcName === '') {
+            throw new \RuntimeException('Invalid custom func policy value');
+        }
+        $result = CustomFuncFactory::getCustomFunc($funcName)->call($this, $this->getUser(), $this->getPolicy());
         if ($result instanceof Collection) {
             return $result;
         }
         throw new \RuntimeException('Custom func must return Collection');
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Rule/Executable/AbstractExecutable.php` around
lines 58 - 64, 在 loadCustomFunc(Policy $policy) 中对 $policy->value[0] 做防御性校验:在调用
CustomFuncFactory::getCustomFunc(...) 之前先确认 $policy->value 是数组或
ArrayAccess、isset($policy->value[0]) 且值为期望类型(例如非空字符串或标识符),否则抛出明确的异常(如
InvalidArgumentException 或 RuntimeException 带描述信息);保留对
CustomFuncFactory::getCustomFunc、返回结果为 Collection 的现有检查和异常抛出逻辑。
app/Repository/Permission/LeaderRepository.php-28-36 (1)

28-36: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

批量操作以避免 N+1 查询问题

create() 方法在循环中逐条删除和创建记录(第 30-32 行),存在严重的 N+1 查询问题。应该使用批量操作来提高性能。此外,手动设置 created_at 会绕过框架的时间戳管理机制。

♻️ 建议的批量操作优化
 public function create(array $data): mixed
 {
-    foreach ($data['user_id'] as $id) {
-        Leader::query()->where('dept_id', $data['dept_id'])->where('user_id', $id)->forceDelete();
-        Leader::create(['dept_id' => $data['dept_id'], 'user_id' => $id, 'created_at' => date('Y-m-d H:i:s')]);
-    }
+    // 批量删除已存在的记录
+    Leader::query()
+        ->where('dept_id', $data['dept_id'])
+        ->whereIn('user_id', $data['user_id'])
+        ->forceDelete();
+    
+    // 批量插入新记录,让框架自动管理时间戳
+    $insertData = array_map(fn($userId) => [
+        'dept_id' => $data['dept_id'],
+        'user_id' => $userId,
+    ], $data['user_id']);
+    
+    Leader::query()->insert($insertData);
+    
     // `@phpstan-ignore-next-line`
     return null;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Repository/Permission/LeaderRepository.php` around lines 28 - 36, The
create() method currently does per-user deletes/inserts causing N+1 queries;
change it to perform a single bulk delete using
Leader::query()->where('dept_id', $data['dept_id'])->whereIn('user_id',
$data['user_id'])->forceDelete(), then build a single array of rows for all
user_ids and insert them in one operation (e.g., Leader::insert($rows)) instead
of calling Leader::create() inside the loop; when preparing $rows, set
timestamps consistently (use a single now() value) rather than setting
created_at per-iteration to avoid bypassing timestamp management and reduce DB
round-trips.
config/autoload/department/custom.php-20-53 (1)

20-53: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

避免重复查询用户部门 ID

在第 37、41、47 行多次调用 $user->department()->get()->pluck('id'),每次都会执行数据库查询。应该在 switch 语句前缓存查询结果。

♻️ 优化建议
 'testction' =>  function (Builder $builder, ScopeType $scopeType, Policy $policy, User $user) {
     // 只针对 id 为 2 的用户生效
     if ($user->id !== 2) {
         return;
     }
     // 获取当前上下文中的创建人字段名称
     $createdByColumn = Context::getCreatedByColumn();
     // 获取当前上下文中的部门字段名称
     $deptColumn = Context::getDeptColumn();
+    // 缓存用户部门 ID
+    $userDeptIds = $user->department()->get()->pluck('id');
+    
     switch ($scopeType){
         // 隔离类型为根据创建人
         case ScopeType::CREATED_BY:
             // 创建人字段为当前用户
             $builder->where($createdByColumn, $user->id);
             break;
         case ScopeType::DEPT:
             // 部门字段为当前用户部门
-            $builder->whereIn($deptColumn, $user->department()->get()->pluck('id'));
+            $builder->whereIn($deptColumn, $userDeptIds);
             break;
         case ScopeType::DEPT_CREATED_BY:
             // 部门字段为当前用户部门
-            $builder->whereIn($deptColumn, $user->department()->get()->pluck('id'));
+            $builder->whereIn($deptColumn, $userDeptIds);
             // 创建人为当前用户
             $builder->where($createdByColumn, $user->id);
             break;
         case ScopeType::DEPT_OR_CREATED_BY:
             // 部门字段为当前用户部门
-            $builder->whereIn($deptColumn, $user->department()->get()->pluck('id'));
+            $builder->whereIn($deptColumn, $userDeptIds);
             // 创建人为当前用户
             $builder->orWhere($createdByColumn, $user->id);
             break;
     }
-    return;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/autoload/department/custom.php` around lines 20 - 53, The closure
registered under 'testction' repeatedly calls
$user->department()->get()->pluck('id') causing multiple DB queries; before the
switch in that closure, cache the department IDs (e.g., $deptIds =
$user->department()->get()->pluck('id')) and replace each
$user->department()->get()->pluck('id') in the ScopeType::DEPT,
::DEPT_CREATED_BY, and ::DEPT_OR_CREATED_BY cases with that cached $deptIds
variable, keeping all other logic ($builder, $createdByColumn, $deptColumn, and
return) unchanged.
web/src/modules/base/views/permission/user/data/getFormItems.tsx-146-160 (1)

146-160: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

移除/清空部门时没有同步 deptIds

deptIdsonNodeClick 的判定依据,但这里删 tag / clear 时没有一起更新。结果是 UI 已经取消选中,内部仍认为它“已选中”,下一次点击会走到移除分支,部门需要点两次才能重新选回。

💡 建议修复
         onRemoveTag: (value: number) => {
+          deptIds.value = deptIds.value.filter((id: number) => id !== value)
           const current = postList.value.find((item: any) => item.id === value)?.positions ?? []
           if (current.length > 0) {
             current?.map((item: any) => {
               if (model.position?.includes(item.id)) {
                 model.position?.splice(model.position?.indexOf(item.id), 1)
@@
         },
         onClear: () => {
+          deptIds.value = []
           postList.value = []
           model.position = []
         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/user/data/getFormItems.tsx` around
lines 146 - 160, When removing tags or clearing posts, the code updates
postList.value and model.position but does not update deptIds used by
onNodeClick, causing stale selection state; in the onRemoveTag handler (and
onClear) update the deptIds collection to remove any department IDs associated
with the removed positions (e.g., for onRemoveTag, iterate the removed item's
positions and remove those ids from deptIds if present; for onClear, set deptIds
to an empty array) and ensure you mutate model.position consistently (use
indexOf before splice) so the UI selection and the deptIds state stay in sync
with postList.value; reference handlers onRemoveTag and onClear and the deptIds
variable checked by onNodeClick.
app/Library/DataPermission/Aspects/DataScopeAspect.php-100-117 (1)

100-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

异常路径会遗留数据权限上下文。

这里的清理只在正常返回时执行。$proceedingJoinPoint->process() 一旦抛异常,data_permission 标记就会残留到后续查询里,权限条件会串请求内的后续逻辑。

💡 建议修复
         DataPermissionContext::setDeptColumn($attribute->getDeptColumn());
         DataPermissionContext::setCreatedByColumn($attribute->getCreatedByColumn());
         DataPermissionContext::setScopeType($attribute->getScopeType());
         DataPermissionContext::setOnlyTables($attribute->getOnlyTables());
-        $result = $proceedingJoinPoint->process();
-        Context::destroy(self::CONTEXT_KEY);
-        return $result;
+        try {
+            return $proceedingJoinPoint->process();
+        } finally {
+            Context::destroy(self::CONTEXT_KEY);
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Aspects/DataScopeAspect.php` around lines 100 -
117, Wrap the call to $proceedingJoinPoint->process() in a try/finally so
cleanup always runs on both success and exception: after setting
Context::set(self::CONTEXT_KEY) and the DataPermissionContext via
DataPermissionContext::setDeptColumn(...), ::setCreatedByColumn(...),
::setScopeType(...), ::setOnlyTables(...), call $proceedingJoinPoint->process()
inside try and in finally always call Context::destroy(self::CONTEXT_KEY) and
also reset the DataPermissionContext (e.g. call the corresponding setters with
null/defaults or a DataPermissionContext::clear() method if available) to ensure
no permission state is leaked to later logic when an exception is thrown.
app/Service/Permission/UserService.php-88-99 (1)

88-99: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

updateById() 的返回值和缓存一致性都丢了。

事务闭包里没有 return,这个方法现在会返回 null。另外更新成功后也没有失效 getInfo() 的缓存键,后续一段时间内仍可能读到旧用户数据。

💡 建议修复
     public function updateById(mixed $id, array $data): mixed
     {
-        return Db::transaction(function () use ($id, $data) {
+        $entity = Db::transaction(function () use ($id, $data) {
             /** `@var` null|User $entity */
             $entity = $this->repository->findById($id);
             if (empty($entity)) {
                 throw new BusinessException(ResultCode::NOT_FOUND);
             }
             $entity->fill($data)->save();
             $this->handleWith($entity, $data);
-        });
+            return $entity;
+        });
+
+        $this->cache->delete((string) $entity->id);
+
+        return $entity;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Service/Permission/UserService.php` around lines 88 - 99, The updateById
method currently returns null because the transaction closure lacks a return and
it also fails to invalidate the cached getInfo result; modify updateById so the
transaction closure returns the updated User entity (e.g. return $entity after
$this->handleWith($entity, $data)) and then invalidate the getInfo cache key for
that user (call the same cache invalidation used elsewhere — e.g. delete the
getInfo cache key via your cache helper or
$this->cache->delete($this->getInfoCacheKey($id)) or a dedicated method) so
subsequent reads see the updated data; ensure these changes are applied inside
the Db::transaction closure and the method returns the updated entity.
app/Library/DataPermission/Aspects/DataScopeAspect.php-104-107 (1)

104-107: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

方法级 #[DataScope] 现在无法覆盖类级配置。

当前优先取类注解,导致同一个类里给单个方法单独配置更窄/更宽的数据范围时不会生效。这里通常应当让方法级优先。

💡 建议修复
-        $attribute = Arr::get($proceedingJoinPoint->getAnnotationMetadata()->class, DataScope::class);
-        if ($attribute === null) {
-            $attribute = Arr::get($proceedingJoinPoint->getAnnotationMetadata()->method, DataScope::class);
-        }
+        $attribute = Arr::get($proceedingJoinPoint->getAnnotationMetadata()->method, DataScope::class)
+            ?? Arr::get($proceedingJoinPoint->getAnnotationMetadata()->class, DataScope::class);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Aspects/DataScopeAspect.php` around lines 104 -
107, The code currently picks class-level DataScope first so method-level
#[DataScope] cannot override it; change the lookup in DataScopeAspect to prefer
the method annotation: first check
Arr::get($proceedingJoinPoint->getAnnotationMetadata()->method,
DataScope::class) and if that is null fall back to the class annotation
(Arr::get(...->class,...)), ensuring $attribute is set from the method when
present so method-level DataScope overrides class-level settings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f88d585-d157-4739-8468-2f1033bf4137

📥 Commits

Reviewing files that changed from the base of the PR and between 7c75b38 and d77e6b6.

📒 Files selected for processing (91)
  • README.md
  • app/Http/Admin/Controller/Permission/DepartmentController.php
  • app/Http/Admin/Controller/Permission/LeaderController.php
  • app/Http/Admin/Controller/Permission/PositionController.php
  • app/Http/Admin/Request/Permission/BatchGrantDataPermissionForPositionRequest.php
  • app/Http/Admin/Request/Permission/DepartmentRequest.php
  • app/Http/Admin/Request/Permission/LeaderRequest.php
  • app/Http/Admin/Request/Permission/PositionRequest.php
  • app/Http/Admin/Request/Permission/RoleRequest.php
  • app/Http/Admin/Request/Permission/UserRequest.php
  • app/Http/CurrentUser.php
  • app/Library/DataPermission/Aspects/DataScopeAspect.php
  • app/Library/DataPermission/Attribute/DataScope.php
  • app/Library/DataPermission/Context.php
  • app/Library/DataPermission/Factory.php
  • app/Library/DataPermission/Manager.php
  • app/Library/DataPermission/Rule/CustomFuncFactory.php
  • app/Library/DataPermission/Rule/Executable/AbstractExecutable.php
  • app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php
  • app/Library/DataPermission/Rule/Executable/DeptExecute.php
  • app/Library/DataPermission/Rule/Rule.php
  • app/Library/DataPermission/Scope/DataScope.php
  • app/Library/DataPermission/Scope/DataScopes.php
  • app/Library/DataPermission/ScopeType.php
  • app/Model/DataPermission/Policy.php
  • app/Model/Enums/DataPermission/PolicyType.php
  • app/Model/Permission/Department.php
  • app/Model/Permission/Leader.php
  • app/Model/Permission/Position.php
  • app/Model/Permission/User.php
  • app/Repository/Permission/DepartmentRepository.php
  • app/Repository/Permission/LeaderRepository.php
  • app/Repository/Permission/PositionRepository.php
  • app/Repository/Permission/RoleRepository.php
  • app/Repository/Permission/UserRepository.php
  • app/Schema/DepartmentSchema.php
  • app/Schema/LeaderSchema.php
  • app/Schema/PolicySchema.php
  • app/Schema/PositionSchema.php
  • app/Schema/RoleSchema.php
  • app/Schema/UserSchema.php
  • app/Service/Permission/DepartmentService.php
  • app/Service/Permission/LeaderService.php
  • app/Service/Permission/PositionService.php
  • app/Service/Permission/UserService.php
  • config/autoload/casbin/rbac-model.conf
  • config/autoload/department/cache.php
  • config/autoload/department/custom.php
  • config/config.php
  • databases/migrations/2025_02_24_195620_create_department_tables.php
  • databases/seeders/menu_seeder_20240926.php
  • databases/seeders/user_dept_20250310.php
  • phpstan.neon.dist
  • storage/languages/zh_CN/user.php
  • tests/Feature/Library/DataPermission/DataScopeAspectTest.php
  • web/src/components/ma-dialog/index.vue
  • web/src/components/ma-resource-picker/panel.vue
  • web/src/components/ma-tree/index.vue
  • web/src/components/ma-upload-image/index.vue
  • web/src/layouts/components/bars/toolbar/components/user-bar.tsx
  • web/src/layouts/uc.tsx
  • web/src/locales/en[English].yaml
  • web/src/locales/zh_CN[简体中文].yaml
  • web/src/locales/zh_TW[繁體中文].yaml
  • web/src/modules/base/api/department.ts
  • web/src/modules/base/api/leader.ts
  • web/src/modules/base/api/position.ts
  • web/src/modules/base/api/user.ts
  • web/src/modules/base/locales/en[English].yaml
  • web/src/modules/base/locales/zh_CN[简体中文].yaml
  • web/src/modules/base/locales/zh_TW[繁體中文].yaml
  • web/src/modules/base/views/permission/component/dataScope.vue
  • web/src/modules/base/views/permission/department/data/getFormItems.tsx
  • web/src/modules/base/views/permission/department/data/getSearchItems.tsx
  • web/src/modules/base/views/permission/department/data/getTableColumns.tsx
  • web/src/modules/base/views/permission/department/form.vue
  • web/src/modules/base/views/permission/department/index.vue
  • web/src/modules/base/views/permission/department/position.vue
  • web/src/modules/base/views/permission/department/setLeader.vue
  • web/src/modules/base/views/permission/department/viewUser.vue
  • web/src/modules/base/views/permission/menu/index.vue
  • web/src/modules/base/views/permission/user/data/getFormItems.tsx
  • web/src/modules/base/views/permission/user/data/getTableColumns.tsx
  • web/src/modules/base/views/permission/user/form.vue
  • web/src/modules/base/views/permission/user/index.vue
  • web/src/plugins/mine-admin/app-store/views/detail.vue
  • web/src/plugins/mine-admin/app-store/views/list.vue
  • web/src/provider/dictionary/data/data-scope.ts
  • web/src/utils/download.ts
  • web/src/utils/imageUrl.ts
  • web/types/components.d.ts
💤 Files with no reviewable changes (2)
  • config/autoload/casbin/rbac-model.conf
  • databases/seeders/menu_seeder_20240926.php

Comment on lines +59 to +119
if ($policyType->isNotCustomFunc() && $policyType->isNotCustomDept()) {
/**
* @var Collection|Department[] $leaderDepartmentList
*/
$leaderDepartmentList = $this->getUser()->department()->get();
foreach ($leaderDepartmentList as $department) {
if ($policyType->isDeptSelf()) {
// @phpstan-ignore-next-line
$this->getUser()->newQuery()
->whereHas('department', static function ($query) use ($department) {
$query->where('id', $department->id);
// @phpstan-ignore-next-line
})->get()->each(static function (User $user) use (&$ids) {
$ids[] = $user->id;
});
}
if ($policyType->isDeptTree()) {
// @phpstan-ignore-next-line
$department->getFlatChildren()->each(function (Department $department) use (&$ids) {
$this->getUser()->newQuery()
->whereHas('department', static function ($query) use ($department) {
$query->where('id', $department->id);
// @phpstan-ignore-next-line
})->get()->each(static function (User $user) use (&$ids) {
$ids[] = $user->id;
});
});
}
}
/**
* @var Collection|Position[] $positionList
*/
$positionList = $this->getUser()->position()->get();
foreach ($positionList as $position) {
if ($policyType->isDeptSelf()) {
// @phpstan-ignore-next-line
$position->department()->get()->each(function (Department $department) use (&$ids) {
$this->getUser()->newQuery()
->whereHas('department', static function ($query) use ($department) {
$query->where('id', $department->id);
// @phpstan-ignore-next-line
})->get()->each(static function (User $user) use (&$ids) {
$ids[] = $user->id;
});
});
}
if ($policyType->isDeptTree()) {
// @phpstan-ignore-next-line
$position->department()->get()->each(function (Department $department) use (&$ids) {
$department->getFlatChildren()->each(function (Department $department) use (&$ids) {
$this->getUser()->newQuery()
->whereHas('department', static function ($query) use ($department) {
$query->where('id', $department->id);
// @phpstan-ignore-next-line
})->get()->each(static function (User $user) use (&$ids) {
$ids[] = $user->id;
});
});
});
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=php -C5 'CreatedByIdsExecute|getCreatedByList'
rg -nP --type=php -C3 'isCustomDept|isCustomFunc'

Repository: mineadmin/MineAdmin

Length of output: 10101


🏁 Script executed:

cat -n app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php

Repository: mineadmin/MineAdmin

Length of output: 7731


🏁 Script executed:

find . -type f -name "*test*" -o -name "*Test*" | grep -i "permission\|datapermission" | head -20

Repository: mineadmin/MineAdmin

Length of output: 345


🏁 Script executed:

cat -n tests/Feature/Library/DataPermission/DataScopeAspectTest.php | head -100

Repository: mineadmin/MineAdmin

Length of output: 3933


🏁 Script executed:

cat -n tests/Feature/Library/DataPermission/DataScopeAspectTest.php | tail -100

Repository: mineadmin/MineAdmin

Length of output: 4680


🏁 Script executed:

rg -n "CustomDept|CustomFunc" tests/ --type=php -A 3 -B 3

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg -n "CreatedByIdsExecute" tests/ --type=php -A 3 -B 3

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

cat -n app/Library/DataPermission/Rule/Executable/DeptExecute.php

Repository: mineadmin/MineAdmin

Length of output: 2625


🏁 Script executed:

rg -n "loadCustomFunc" app/ --type=php -A 2 -B 2

Repository: mineadmin/MineAdmin

Length of output: 2547


🏁 Script executed:

cat -n app/Library/DataPermission/Rule/Executable/AbstractExecutable.php | sed -n '50,65p'

Repository: mineadmin/MineAdmin

Length of output: 677


修复CustomDept和CustomFunc策略下的成员权限丢失bug

CreatedByIdsExecute.php中存在严重的逻辑漏洞:第33-35行加载CustomDept部门、第49-51行加载CustomFunc返回的部门,但这些加载的$departmentList永远不会被使用。

根本原因是第59行的条件if ($policyType->isNotCustomFunc() && $policyType->isNotCustomDept())将这两种策略完全排除。第121-146行对$departmentList的处理被放在此条件块内部,导致:

  • CustomDept策略:第34行加载的部门列表被丢弃,最终只返回[当前用户id],而非自定义部门下的所有成员
  • CustomFunc策略:第50行加载的部门集合被丢弃,最终只返回[当前用户id],而非函数返回部门下的所有成员

这直接破坏了数据权限的预期行为。对比DeptExecute.php的实现(第32-37行加载不同策略的部门,第51-62行统一处理,不排除任何策略类型),应将"根据$departmentList派生用户IDs"的逻辑提取到所有非Self/All策略的分支后统一执行,而非嵌入排除CustomDept/CustomFunc的条件块中。

当前不存在覆盖CustomDept和CustomFunc路径的测试用例,建议补充测试确保修复后这两种策略能正确返回部门成员user id。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Rule/Executable/CreatedByIdsExecute.php` around
lines 59 - 119, The code in CreatedByIdsExecute.php incorrectly excludes
CustomDept and CustomFunc branches by wrapping the department-to-user-id
derivation inside if ($policyType->isNotCustomFunc() &&
$policyType->isNotCustomDept()), so $departmentList loaded for
CustomDept/CustomFunc is never used; fix by moving the logic that converts
$departmentList into user IDs (the blocks that call getFlatChildren(),
whereHas('department') and push into $ids) out of that exclusion condition and
into the shared handling path used for all non-Self/non-All policies (mirror
DeptExecute.php behavior), ensuring that department lists loaded for CustomDept
and CustomFunc are iterated and converted to user IDs (respecting
isDeptSelf/isDeptTree checks), update code paths that reference $departmentList
and $positionList (methods: getUser(), department(), position(),
getFlatChildren(), whereHas) accordingly, and add unit tests to cover CustomDept
and CustomFunc so they return members' user ids rather than only the current
user.

Comment on lines +22 to +63
public function execute(): ?array
{
$policy = $this->getPolicy();
$policyType = $policy->policy_type;
if ($policyType->isAll()) {
return null;
}
/*
* @var Department[]|Collection $departmentList
*/
if ($policyType->isCustomDept()) {
$departmentList = $this->getRepository(DepartmentRepository::class)->getByIds($policy->value);
}
if ($policyType->isCustomFunc()) {
$departmentList = $this->loadCustomFunc($policy);
}
if ($policyType->isDeptSelf() || $policyType->isSelf()) {
$departmentList = $this->getUser()->department()->get();
}
if ($policyType->isDeptTree()) {
$departmentList = collect();
/**
* @var Collection|Department[] $currentList
*/
$currentList = $this->getUser()->department()->get();
foreach ($currentList as $item) {
$departmentList = $departmentList->merge($item->getFlatChildren());
}
}
if (empty($departmentList)) {
return null;
}
$departmentList = $departmentList->merge($this->getUser()->dept_leader()->get());
/**
* @var Collection|Position[] $positionList
*/
$positionList = $this->getUser()->position()->get();
foreach ($positionList as $position) {
$departmentList = $departmentList->merge($position->department()->get());
}
return $departmentList->pluck('id')->toArray();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

未初始化变量导致潜在的运行时错误

第 32-50 行的多个 if 语句有条件地设置 $departmentList,但如果策略类型不匹配任何条件,$departmentList 将保持未定义状态。这会在第 51 行的 empty($departmentList) 检查和第 54 行的方法调用时导致"未定义变量"错误。

🐛 建议的修复方案

在条件检查之前初始化 $departmentList

 public function execute(): ?array
 {
     $policy = $this->getPolicy();
     $policyType = $policy->policy_type;
     if ($policyType->isAll()) {
         return null;
     }
+    $departmentList = collect();
     /*
      * `@var` Department[]|Collection $departmentList
      */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Rule/Executable/DeptExecute.php` around lines 22 -
63, The execute() method may use $departmentList before it's set; initialize
$departmentList to an empty Collection at the start of the method (e.g., before
checking $policyType) so all branches can safely merge into it and empty
checks/merge calls (later uses in execute(), including pluck('id')) won't
trigger "undefined variable" errors; ensure the initialized type matches
expected operations (a Collection) so subsequent merges, get() results, and
pluck() work correctly.

Comment on lines +53 to +59
$cacheKey = $this->getPrefix() . ':deptIds:' . $user->id;
if ($this->isCache() && $this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$execute = new DeptExecute($user, $policy);
$result = $execute->execute();
$this->cache->set($cacheKey, $result, $this->getTtl());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

缓存键缺少 Policy 维度。

这两个结果都依赖 $policy,但当前 key 只拼了 $user->id。同一用户切换不同策略,或者策略内容更新后,都会直接命中旧权限范围,导致数据权限串用。

💡 建议修复
-        $cacheKey = $this->getPrefix() . ':deptIds:' . $user->id;
+        $cacheKey = $this->getPrefix() . ':deptIds:' . $user->id . ':' . $policy->id;
         if ($this->isCache() && $this->cache->has($cacheKey)) {
             return $this->cache->get($cacheKey);
         }
         $execute = new DeptExecute($user, $policy);
         $result = $execute->execute();
-        $this->cache->set($cacheKey, $result, $this->getTtl());
+        if ($this->isCache()) {
+            $this->cache->set($cacheKey, $result, $this->getTtl());
+        }
         return $result;
-        $cacheKey = $this->getPrefix() . ':createdBy:' . $user->id;
+        $cacheKey = $this->getPrefix() . ':createdBy:' . $user->id . ':' . $policy->id;
         if ($this->isCache() && $this->cache->has($cacheKey)) {
             return $this->cache->get($cacheKey);
         }
         $execute = new CreatedByIdsExecute($user, $policy);
         $result = $execute->execute();
-        $this->cache->set($cacheKey, $result, $this->getTtl());
+        if ($this->isCache()) {
+            $this->cache->set($cacheKey, $result, $this->getTtl());
+        }
         return $result;

Also applies to: 65-71

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Library/DataPermission/Rule/Rule.php` around lines 53 - 59, The cache key
for dept IDs omits the Policy dimension so different policies share cached
results; update the key generation in Rule::get (and the similar block at 65-71)
to incorporate a stable policy identifier (e.g. $policy->id or a hash/serialized
representation of $policy) when building $cacheKey (used with getPrefix(),
isCache(), cache->has(), cache->get(), cache->set()), so DeptExecute($user,
$policy)->execute() results are cached per-policy and the cache->set($cacheKey,
$result, $this->getTtl()) uses that policy-aware key.

Comment on lines +26 to +44
#[Property(property: 'policy_type', title: '策略类型', type: 'string')]
protected PolicyType $policyType;

#[Property(property: 'is_default', title: '是否默认策略', type: 'bool')]
protected bool $isDefault;

#[Property(property: 'value', title: '策略值', type: 'object')]
protected array $value;

public function __construct(protected Policy $model) {}

public function jsonSerialize(): mixed
{
return [
'policy_type' => $this->policyType->value,
'is_default' => $this->isDefault,
'value' => $this->value,
];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

严重问题:类型属性从未初始化,jsonSerialize() 在运行时会抛错。

$policyType$isDefault$value 都是已声明的 typed properties,但构造函数只保存了 $model,从未给它们赋值。一旦调用 jsonSerialize(),PHP 8+ 将抛出 Error: Typed property App\Schema\PolicySchema::$policyType must not be accessed before initialization,导致 Swagger 文档以外的所有真实序列化路径(例如在 RoleSchema/UserSchema 中嵌入序列化时)直接失败。

应该让 jsonSerialize() 直接读取 $this->model,或在构造函数里把模型字段同步到属性。

🐛 建议修复
-    public function __construct(protected Policy $model) {}
-
     public function jsonSerialize(): mixed
     {
         return [
-            'policy_type' => $this->policyType->value,
-            'is_default' => $this->isDefault,
-            'value' => $this->value,
+            'policy_type' => $this->model->policy_type->value,
+            'is_default' => $this->model->is_default,
+            'value' => $this->model->value,
         ];
     }

或者改为在构造函数里赋值:

-    public function __construct(protected Policy $model) {}
+    public function __construct(protected Policy $model)
+    {
+        $this->policyType = $model->policy_type;
+        $this->isDefault  = $model->is_default;
+        $this->value      = $model->value ?? [];
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[Property(property: 'policy_type', title: '策略类型', type: 'string')]
protected PolicyType $policyType;
#[Property(property: 'is_default', title: '是否默认策略', type: 'bool')]
protected bool $isDefault;
#[Property(property: 'value', title: '策略值', type: 'object')]
protected array $value;
public function __construct(protected Policy $model) {}
public function jsonSerialize(): mixed
{
return [
'policy_type' => $this->policyType->value,
'is_default' => $this->isDefault,
'value' => $this->value,
];
}
#[Property(property: 'policy_type', title: '策略类型', type: 'string')]
protected PolicyType $policyType;
#[Property(property: 'is_default', title: '是否默认策略', type: 'bool')]
protected bool $isDefault;
#[Property(property: 'value', title: '策略值', type: 'object')]
protected array $value;
public function __construct(protected Policy $model)
{
$this->policyType = $model->policy_type;
$this->isDefault = $model->is_default;
$this->value = $model->value ?? [];
}
public function jsonSerialize(): mixed
{
return [
'policy_type' => $this->policyType->value,
'is_default' => $this->isDefault,
'value' => $this->value,
];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Schema/PolicySchema.php` around lines 26 - 44, 构造函数只保存了传入的 Policy 到
$model,但已声明的属性 $policyType、$isDefault、$value 未被初始化,导致
PolicySchema::jsonSerialize() 访问未初始化的 typed properties 抛错;修复方法:在
PolicySchema::__construct(Policy $model) 中将 $this->policyType =
$model->policy_type (或对应枚举/字段)、$this->isDefault =
(bool)$model->is_default、$this->value = (array)$model->value 等同步赋值;或者替代方案是在
jsonSerialize() 中直接从 $this->model 读取并返回 ['policy_type' => $this->model->... ,
'is_default' => ..., 'value' => ...],确保不再访问未初始化的属性。

Comment on lines +45 to +48
public function jsonSerialize(): mixed
{
return [];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

严重问题:jsonSerialize() 返回空数组,所有使用该 Schema 序列化的接口都会输出 {}

这显然不是预期实现:Swagger 注解描述了一系列字段(idnamedept_iddepartment 等),但运行时序列化产生的 JSON 完全为空,会让前端拿到空岗位对象。

需要让 jsonSerialize() 实际返回这些属性的内容,或参考 PolicySchema 的写法持有一个 Position $model 并在序列化时读取模型字段。同时,所有 public ?int $id; 这类已声明但从未初始化的 typed property,在直接读取时会触发 Typed property must not be accessed before initialization,因此推荐改为读取模型。

🐛 建议修复(参考 PolicySchema 写法)
-class PositionSchema implements \JsonSerializable
-{
-    #[Property(property: 'id', title: '主键', type: 'int')]
-    public ?int $id;
+class PositionSchema implements \JsonSerializable
+{
+    public function __construct(protected Position $model) {}
+
+    #[Property(property: 'id', title: '主键', type: 'integer')]
+    public ?int $id;
     ...
-    public function jsonSerialize(): mixed
-    {
-        return [];
-    }
+    public function jsonSerialize(): mixed
+    {
+        return [
+            'id'         => $this->model->id,
+            'name'       => $this->model->name,
+            'dept_id'    => $this->model->dept_id,
+            'created_by' => $this->model->created_by,
+            'updated_by' => $this->model->updated_by,
+            'created_at' => $this->model->created_at,
+            'updated_at' => $this->model->updated_at,
+            'department' => $this->model->relationLoaded('department') ? $this->model->department : null,
+        ];
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Schema/PositionSchema.php` around lines 45 - 48, The jsonSerialize() in
PositionSchema currently returns an empty array causing all serialized Position
responses to be {} and also risks accessing uninitialized typed properties like
public ?int $id;; fix by mirroring PolicySchema: add a private Position $model
property to PositionSchema (or accept Position in the constructor), remove
direct reads of the typed public properties, and implement jsonSerialize() to
return an associative array that reads values from $this->model (id, name,
dept_id, department, etc.) so serialization returns the actual model fields and
avoids "Typed property must not be accessed before initialization" errors.

Comment thread app/Schema/RoleSchema.php
public function jsonSerialize(): mixed
{
return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark];
return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy->jsonSerialize()];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

policy 可空但序列化时未做空判断,会导致运行时错误。

Line 57 定义的是 ?PolicySchema,Line 72 也可能赋 null;Line 77 直接调用方法会在空值场景下报错。

建议修复
- return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy->jsonSerialize()];
+ return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy?->jsonSerialize()];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy->jsonSerialize()];
return ['id' => $this->id, 'name' => $this->name, 'code' => $this->code, 'data_scope' => $this->dataScope, 'status' => $this->status, 'sort' => $this->sort, 'created_by' => $this->createdBy, 'updated_by' => $this->updatedBy, 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, 'remark' => $this->remark, 'policy' => $this->policy?->jsonSerialize()];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Schema/RoleSchema.php` at line 77, RoleSchema 的 policy 属性为 ?PolicySchema
可能为 null,但在 toArray/serialization 返回中直接调用 $this->policy->jsonSerialize() 会在为
null 时触发运行时错误;修改 RoleSchema 的序列化返回(当前 return 表达式)在访问 $this->policy 前做空判断(例如三元或
null 合并)并在为 null 时返回 null 或空数组,确保引用 $this->policy->jsonSerialize() 仅在
$this->policy !== null 时调用,以避免空指针异常。

Comment on lines +40 to +49
public function updateById(mixed $id, array $data): mixed
{
return Db::transaction(function () use ($id, $data) {
$entity = $this->repository->findById($id);
if (empty($entity)) {
throw new BusinessException(ResultCode::NOT_FOUND);
}
$this->handleEntity($entity, $data);
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

缺少实体属性更新操作

updateById() 方法只调用了 handleEntity() 来同步关系,但没有更新实体本身的属性(如 nameparent_id 等)。这会导致更新操作无法修改部门的基本信息。

🐛 修复建议
 public function updateById(mixed $id, array $data): mixed
 {
     return Db::transaction(function () use ($id, $data) {
         $entity = $this->repository->findById($id);
         if (empty($entity)) {
             throw new BusinessException(ResultCode::NOT_FOUND);
         }
+        // 更新实体属性
+        $this->repository->updateById($id, $data);
         $this->handleEntity($entity, $data);
     });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function updateById(mixed $id, array $data): mixed
{
return Db::transaction(function () use ($id, $data) {
$entity = $this->repository->findById($id);
if (empty($entity)) {
throw new BusinessException(ResultCode::NOT_FOUND);
}
$this->handleEntity($entity, $data);
});
}
public function updateById(mixed $id, array $data): mixed
{
return Db::transaction(function () use ($id, $data) {
$entity = $this->repository->findById($id);
if (empty($entity)) {
throw new BusinessException(ResultCode::NOT_FOUND);
}
// 更新实体属性
$this->repository->updateById($id, $data);
$this->handleEntity($entity, $data);
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Service/Permission/DepartmentService.php` around lines 40 - 49, The
updateById method currently only calls handleEntity to sync relations but never
applies or persists the entity's scalar attributes; inside the Db::transaction
closure (in updateById) load the entity via $this->repository->findById($id),
throw BusinessException(ResultCode::NOT_FOUND) if empty as already done, then
copy the updatable scalar fields from $data (e.g. name, parent_id, other attrs)
onto the $entity (or call the entity's fill/update method), persist those
changes via the repository (e.g. $this->repository->save/update/flush) and then
call handleEntity($entity, $data) to sync relations before committing the
transaction so basic department fields are actually updated.

Comment on lines +29 to +37
public function deleteByDoubleKey(array $data): bool
{
try {
$this->repository->deleteByDoubleKey($data['dept_id'], $data['user_ids']);
return true;
} catch (Exception $e) {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

不应在生产代码中捕获测试库异常

第 34 行捕获的 Mockery\Exception 是来自 Mockery 测试库的异常类型,不应在生产代码中使用。这意味着如果在运行时抛出其他类型的异常,它们将不会被捕获,可能导致未处理的异常。

🐛 建议的修复方案

使用通用异常类型:

 use App\Model\Permission\Leader;
 use App\Repository\Permission\LeaderRepository;
 use App\Service\IService;
-use Mockery\Exception;

 /**
  * `@extends` IService<Leader>
  */
 class LeaderService extends IService
 {
     public function __construct(
         protected readonly LeaderRepository $repository
     ) {}

     public function deleteByDoubleKey(array $data): bool
     {
         try {
             $this->repository->deleteByDoubleKey($data['dept_id'], $data['user_ids']);
             return true;
-        } catch (Exception $e) {
+        } catch (\Throwable $e) {
             return false;
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Service/Permission/LeaderService.php` around lines 29 - 37, The catch in
deleteByDoubleKey currently resolves to Mockery\Exception (via an imported
Exception), so replace it with a general runtime catcher: change the method to
catch a global exception type (e.g., \Throwable or \Exception with a leading
backslash) and ensure no Mockery\Exception import shadows it; keep the
repository call $this->repository->deleteByDoubleKey(...) and return false on
failure as before.

Comment thread config/config.php
'app_name' => env('APP_NAME', 'MineAdmin'),
'scan_cacheable' => ! env('APP_DEBUG', false),
'debug' => env('APP_DEBUG', false),
'debug' => env('APP_DEBUG', true),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚖️ Poor tradeoff

将调试模式默认值改为 true 存在安全风险。

debug 的默认值从 false 改为 true 可能导致生产环境意外启用调试模式(当未设置 APP_DEBUG 环境变量时)。调试模式会:

  • 暴露敏感的错误信息和堆栈跟踪
  • 降低应用性能
  • 可能泄露系统内部结构

出于安全考虑,建议保持默认值为 false,仅在开发环境中通过环境变量显式启用。

🔒 建议的安全修复
-    'debug' => env('APP_DEBUG', true),
+    'debug' => env('APP_DEBUG', false),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'debug' => env('APP_DEBUG', true),
'debug' => env('APP_DEBUG', false),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/config.php` at line 18, The config entry for debug currently sets a
risky default ('debug' => env('APP_DEBUG', true)); change this to use a safe
default by setting the env() fallback to false so that the application remains
non-debug by default; update the 'debug' key in config/config.php (the line
containing env('APP_DEBUG', ...)) to use false as the second argument and ensure
any tests or docs note that APP_DEBUG must be explicitly enabled in development.

Comment on lines +46 to +85
ok: ({ formType }, okLoadingState: (state: boolean) => void) => {
okLoadingState(true)
if (['add', 'edit'].includes(formType)) {
const elForm = formRef.value.maForm.getElFormRef()
// 验证通过后
elForm.validate().then(() => {
switch (formType) {
// 新增
case 'add':
formRef.value.add().then((res: any) => {
res.code === ResultCode.SUCCESS ? msg.success(t('crud.createSuccess')) : msg.error(res.message)
maDialog.close()
proTableRef.value.refresh()
}).catch((err: any) => {
msg.alertError(err)
})
break
// 修改
case 'edit':
formRef.value.edit().then((res: any) => {
res.code === 200 ? msg.success(t('crud.updateSuccess')) : msg.error(res.message)
maDialog.close()
proTableRef.value.refresh()
}).catch((err: any) => {
msg.alertError(err)
})
break
}
}).catch()
}
else if (formType === 'position' || formType === 'viewUser') {
proTableRef.value.refresh()
maDialog.close()
}
else {
proTableRef.value.refresh()
maDialog.close()
}
okLoadingState(false)
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

提交按钮的 loading 提前结束了。

okLoadingState(false) 在校验和保存请求完成前就执行了。弹窗确认按钮会过早恢复可点击,新增/编辑都可能被重复提交。

💡 建议修复
-  ok: ({ formType }, okLoadingState: (state: boolean) => void) => {
+  ok: async ({ formType }, okLoadingState: (state: boolean) => void) => {
     okLoadingState(true)
-    if (['add', 'edit'].includes(formType)) {
-      const elForm = formRef.value.maForm.getElFormRef()
-      // 验证通过后
-      elForm.validate().then(() => {
-        switch (formType) {
-          case 'add':
-            formRef.value.add().then((res: any) => {
-              res.code === ResultCode.SUCCESS ? msg.success(t('crud.createSuccess')) : msg.error(res.message)
-              maDialog.close()
-              proTableRef.value.refresh()
-            }).catch((err: any) => {
-              msg.alertError(err)
-            })
-            break
-          case 'edit':
-            formRef.value.edit().then((res: any) => {
-              res.code === 200 ? msg.success(t('crud.updateSuccess')) : msg.error(res.message)
-              maDialog.close()
-              proTableRef.value.refresh()
-            }).catch((err: any) => {
-              msg.alertError(err)
-            })
-            break
-        }
-      }).catch()
-    }
-    else if (formType === 'position' || formType === 'viewUser') {
-      proTableRef.value.refresh()
-      maDialog.close()
-    }
-    else {
-      proTableRef.value.refresh()
-      maDialog.close()
+    try {
+      if (['add', 'edit'].includes(formType)) {
+        const elForm = formRef.value.maForm.getElFormRef()
+        await elForm.validate()
+        const res = formType === 'add'
+          ? await formRef.value.add()
+          : await formRef.value.edit()
+
+        res.code === ResultCode.SUCCESS
+          ? msg.success(formType === 'add' ? t('crud.createSuccess') : t('crud.updateSuccess'))
+          : msg.error(res.message)
+
+        maDialog.close()
+        await proTableRef.value.refresh()
+        return
+      }
+
+      await proTableRef.value.refresh()
+      maDialog.close()
+    } catch (err: any) {
+      if (err) {
+        msg.alertError(err)
+      }
+    } finally {
+      okLoadingState(false)
     }
-    okLoadingState(false)
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ok: ({ formType }, okLoadingState: (state: boolean) => void) => {
okLoadingState(true)
if (['add', 'edit'].includes(formType)) {
const elForm = formRef.value.maForm.getElFormRef()
// 验证通过后
elForm.validate().then(() => {
switch (formType) {
// 新增
case 'add':
formRef.value.add().then((res: any) => {
res.code === ResultCode.SUCCESS ? msg.success(t('crud.createSuccess')) : msg.error(res.message)
maDialog.close()
proTableRef.value.refresh()
}).catch((err: any) => {
msg.alertError(err)
})
break
// 修改
case 'edit':
formRef.value.edit().then((res: any) => {
res.code === 200 ? msg.success(t('crud.updateSuccess')) : msg.error(res.message)
maDialog.close()
proTableRef.value.refresh()
}).catch((err: any) => {
msg.alertError(err)
})
break
}
}).catch()
}
else if (formType === 'position' || formType === 'viewUser') {
proTableRef.value.refresh()
maDialog.close()
}
else {
proTableRef.value.refresh()
maDialog.close()
}
okLoadingState(false)
},
ok: async ({ formType }, okLoadingState: (state: boolean) => void) => {
okLoadingState(true)
try {
if (['add', 'edit'].includes(formType)) {
const elForm = formRef.value.maForm.getElFormRef()
await elForm.validate()
const res = formType === 'add'
? await formRef.value.add()
: await formRef.value.edit()
res.code === ResultCode.SUCCESS
? msg.success(formType === 'add' ? t('crud.createSuccess') : t('crud.updateSuccess'))
: msg.error(res.message)
maDialog.close()
await proTableRef.value.refresh()
return
}
await proTableRef.value.refresh()
maDialog.close()
} catch (err: any) {
if (err) {
msg.alertError(err)
}
} finally {
okLoadingState(false)
}
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/modules/base/views/permission/department/index.vue` around lines 46 -
85, The submit handler ok currently calls okLoadingState(false) immediately,
ending the loading state before form validation and the add/edit requests
complete; fix by keeping the loading state true until the async validation and
request finish (or fail): move the okLoadingState(false) calls into the
completion paths of elForm.validate().then(...) and its inner add()/edit()
promise chains (and their .catch handlers), as well as handle the branches for
formType 'position'/'viewUser' by setting okLoadingState(false) after
proTableRef.value.refresh() and maDialog.close(); ensure every async success and
error path for formRef.value.add(), formRef.value.edit(), and elForm.validate()
ends with okLoadingState(false) so the button stays disabled during the whole
operation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants