feat(project): 系统设置新增「创建项目」权限开关

在系统设置「项目相关」新增「创建项目」权限范围(复选):
所有人 / 部门负责人(含部门管理员)/ 指定人员,三者与「所有人」互斥;
系统管理员始终可创建,不受开关限制。未授权用户隐藏「新建项目」入口
(顶部下拉、快捷键、移动端、应用宫格),后端 Project::userCanCreate()
兜底拦截,个人项目(注册自动创建)不受限。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kuaifan 2026-06-04 13:52:33 +00:00
parent fd6a8a3650
commit 2eee171a50
6 changed files with 96 additions and 0 deletions

View File

@ -69,6 +69,8 @@ class SystemController extends AbstractController
'login_code',
'password_policy',
'project_invite',
'project_add_permission',
'project_add_userids',
'chat_information',
'anon_message',
'convert_video',
@ -154,6 +156,10 @@ class SystemController extends AbstractController
$setting['department_owner_project_view'] = $setting['department_owner_project_view'] ?: 'close';
$setting['server_timezone'] = config('app.timezone');
$setting['server_version'] = Base::getVersion();
// 指定人员名单仅管理员可见
if ($type != 'all' && $type != 'save') {
unset($setting['project_add_userids']);
}
//
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
}

View File

@ -605,6 +605,38 @@ class Project extends AbstractModel
});
}
/**
* 判断用户是否有权限创建项目(依据系统设置「项目创建权限」)
* @param int $userid
* @return bool
*/
public static function userCanCreate($userid)
{
// 范围已在 Setting::getSettingAttribute() 归一化(默认 ['all']
$modes = Base::settingFind('system', 'project_add_permission', ['all']);
// 「所有人」:放行(与具体用户无关,避免未携带身份时被误判为无权)
if (in_array('all', $modes)) {
return true;
}
$user = User::find(intval($userid));
if (empty($user)) {
return false;
}
// 系统管理员始终可创建项目(不受开关限制)
if ($user->isAdmin()) {
return true;
}
// 部门负责人/部门管理员
if (in_array('departmentOwner', $modes) && UserDepartment::getManagedDepartments($user->userid)->isNotEmpty()) {
return true;
}
// 指定人员
if (in_array('appoint', $modes)) {
return in_array($user->userid, Base::settingFind('system', 'project_add_userids', []));
}
return false;
}
/**
* 创建项目
* @param $params
@ -621,6 +653,10 @@ class Project extends AbstractModel
$desc = trim(Arr::get($params, 'desc', ''));
$flow = trim(Arr::get($params, 'flow', 'close'));
$isPersonal = intval(Arr::get($params, 'personal'));
// 个人项目为系统自动创建,不受创建权限限制
if (!$isPersonal && !self::userCanCreate($userid)) {
return Base::retError('当前仅指定人员可以创建项目');
}
if (mb_strlen($name) < 2) {
return Base::retError('项目名称不可以少于2个字');
} elseif (mb_strlen($name) > 32) {

View File

@ -59,6 +59,14 @@ class Setting extends AbstractModel
if (!is_array($value['task_default_time']) || count($value['task_default_time']) != 2 || !Timer::isTime($value['task_default_time'][0]) || !Timer::isTime($value['task_default_time'][1])) {
$value['task_default_time'] = ['09:00', '18:00'];
}
// 项目创建权限范围all/departmentOwner/appoint默认 all+ 指定人员
$value['project_add_permission'] = array_values(array_intersect(
is_array($value['project_add_permission'] ?? null) ? $value['project_add_permission'] : [],
['all', 'departmentOwner', 'appoint']
)) ?: ['all'];
$value['project_add_userids'] = is_array($value['project_add_userids'] ?? null)
? array_values(array_unique(array_filter(array_map('intval', $value['project_add_userids']))))
: [];
break;
// 文件设置

View File

@ -995,3 +995,4 @@ LDAP 用户缺少邮箱属性,请联系管理员配置
发送者昵称最多不能超过20字
AI 助手
没有查看权限
当前仅指定人员可以创建项目

View File

@ -2449,3 +2449,8 @@ AI任务分析
(*)分钟前
(*)小时前
(*)天前
所有人:所有成员均可创建项目。
可创建项目的人员
系统管理员(始终可创建,不受开关限制)。
部门负责人与部门管理员。
下方指定的人员。

View File

@ -75,6 +75,26 @@
</RadioGroup>
<div v-if="formDatum.project_invite == 'open'" class="form-tip">{{$L('开启项目管理员可生成链接邀请成员加入项目')}}</div>
</FormItem>
<FormItem :label="$L('创建项目')" prop="project_add_permission">
<CheckboxGroup :value="formDatum.project_add_permission || []" @on-change="onProjectAddPermissionChange">
<Checkbox label="all">{{$L('所有人')}}</Checkbox>
<Checkbox label="departmentOwner">{{$L('部门负责人')}}</Checkbox>
<Checkbox label="appoint">{{$L('指定人员')}}</Checkbox>
</CheckboxGroup>
<div v-if="projectAddAll" class="form-tip">{{$L('所有人所有成员均可创建项目')}}</div>
<div v-else class="form-tip form-list">
<p>{{$L('可创建项目的人员')}}</p>
<ol>
<li>{{$L('系统管理员(始终可创建,不受开关限制)。')}}</li>
<li v-if="(formDatum.project_add_permission || []).includes('departmentOwner')">{{$L('部门负责人与部门管理员')}}</li>
<li v-if="(formDatum.project_add_permission || []).includes('appoint')">{{$L('下方指定的人员')}}</li>
</ol>
</div>
<template v-if="(formDatum.project_add_permission || []).includes('appoint')">
<UserSelect v-model="formDatum.project_add_userids" :multiple-max="200" avatar-name show-disable :title="$L('请选择指定人员')" style="margin-top:6px"/>
<div class="form-tip">{{$L('指定人员最多可选择200人')}}</div>
</template>
</FormItem>
<FormItem :label="$L('部门负责人视角')" prop="department_owner_project_view">
<RadioGroup v-model="formDatum.department_owner_project_view">
<Radio label="open">{{$L('开启')}}</Radio>
@ -316,10 +336,13 @@
<script>
import {mapState} from "vuex";
import UserSelect from "../../../../components/UserSelect.vue";
export default {
name: 'SystemSetting',
components: {UserSelect},
data() {
return {
loadIng: 0,
@ -334,9 +357,26 @@ export default {
computed: {
...mapState(['formOptions']),
projectAddAll() {
return (this.formDatum.project_add_permission || []).includes('all');
},
},
methods: {
onProjectAddPermissionChange(value) {
const prev = this.formDatum.project_add_permission || [];
let next = value;
if (value.includes('all') && value.length > 1) {
//
next = prev.includes('all') ? value.filter(v => v !== 'all') : ['all'];
}
if (next.length === 0) {
next = ['all'];
}
this.$set(this.formDatum, 'project_add_permission', next);
},
submitForm() {
this.$refs.formDatum.validate((valid) => {
if (valid) {