mirror of
https://github.com/kuaifan/dootask.git
synced 2026-05-24 01:14:06 +00:00
feat(multi-owner): 群/项目/部门支持主+副双负责人体系
This commit is contained in:
parent
24710289e1
commit
64649b514e
@ -2897,7 +2897,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
//
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
||||
// 有群主(主或副)时,仅群主/副群主可邀请;无群主时,任意成员可邀请
|
||||
// 有群主时,仅群主/群管理员可邀请;无群主时,任意成员可邀请
|
||||
if ($dialog->owner_id > 0 && !$dialog->isOwner($user->userid)) {
|
||||
throw new \App\Exceptions\ApiException('仅限群主或群管理员操作');
|
||||
}
|
||||
@ -3013,7 +3013,7 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* 任命副群主(仅主群主可操作)
|
||||
* 任命群管理员(仅群主可操作)
|
||||
*
|
||||
* @apiParam {Number} dialog_id 群对话ID
|
||||
* @apiParam {Number} userid 要任命的群成员 userid
|
||||
@ -3028,7 +3028,7 @@ class DialogController extends AbstractController
|
||||
return Base::retError('请选择有效的成员');
|
||||
}
|
||||
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id, true); // checkOwner=true:仅主群主
|
||||
$dialog = WebSocketDialog::checkDialog($dialog_id, true); // checkOwner=true:仅群主
|
||||
$dialog->checkGroup('user'); // 仅普通群
|
||||
|
||||
$member = WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
@ -3054,10 +3054,10 @@ class DialogController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* 罢免副群主(仅主群主可操作)
|
||||
* 罢免群管理员(仅群主可操作)
|
||||
*
|
||||
* @apiParam {Number} dialog_id 群对话ID
|
||||
* @apiParam {Number} userid 要罢免的副群主 userid
|
||||
* @apiParam {Number} userid 要罢免的群管理员 userid
|
||||
*/
|
||||
public function group__deldeputy()
|
||||
{
|
||||
|
||||
@ -381,7 +381,7 @@ class ProjectController extends AbstractController
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {Number[]} userid 成员userid数组(最终完整列表)
|
||||
* @apiParam {Number[]} [deputy_userid] 副负责人userid数组(可选,仅主负责人有效;必须是 userid 子集)
|
||||
* @apiParam {Number[]} [deputy_userid] 项目管理员userid数组(可选,仅负责人有效;必须是 userid 子集)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@ -408,7 +408,7 @@ class ProjectController extends AbstractController
|
||||
//
|
||||
$project = Project::userProject($project_id, true, true);
|
||||
//
|
||||
// 仅主负责人可设置副负责人;副负责人/其他角色提交 deputy_userid 一律忽略
|
||||
// 仅负责人可设置项目管理员;项目管理员/其他角色提交 deputy_userid 一律忽略
|
||||
$isPrimary = (int)$project->owner === ProjectUser::OWNER_PRIMARY;
|
||||
$applyDeputy = $isPrimary && $deputy_userid !== null;
|
||||
//
|
||||
@ -434,7 +434,7 @@ class ProjectController extends AbstractController
|
||||
$row->exitProject();
|
||||
}
|
||||
//
|
||||
// 副负责人 diff(仅主负责人有效)
|
||||
// 项目管理员 diff(仅负责人有效)
|
||||
if ($applyDeputy) {
|
||||
$currentDeputies = ProjectUser::whereProjectId($project->id)
|
||||
->where('owner', ProjectUser::OWNER_DEPUTY)
|
||||
@ -624,11 +624,11 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
//
|
||||
AbstractModel::transaction(function() use ($owner_userid, $project) {
|
||||
// 仅清除原主 owner=1(副 owner=2 保留)
|
||||
// 仅清除原负责人 owner=1(项目管理员 owner=2 保留)
|
||||
ProjectUser::whereProjectId($project->id)
|
||||
->whereOwner(ProjectUser::OWNER_PRIMARY)
|
||||
->change(['owner' => 0]);
|
||||
// 设新主 owner=1(如新主原本是副,从 2 升为 1)
|
||||
// 设新负责人 owner=1(如新负责人原本是项目管理员,从 2 升为 1)
|
||||
ProjectUser::updateInsert([
|
||||
'project_id' => $project->id,
|
||||
'userid' => $owner_userid,
|
||||
@ -648,7 +648,7 @@ class ProjectController extends AbstractController
|
||||
$project->addLog("移交项目给", ['userid' => $owner_userid]);
|
||||
});
|
||||
//
|
||||
// pushMsg 带 deputy_userids,前端可直接更新副列表无需重拉
|
||||
// pushMsg 带 deputy_userids,前端可直接更新项目管理员列表无需重拉
|
||||
$project->pushMsg('detail', [
|
||||
'owner_userid' => $project->fresh()->owner_userid,
|
||||
'deputy_userids' => $project->fresh()->deputy_userids,
|
||||
@ -657,7 +657,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/project/adddeputy 任命副负责人(仅主负责人可操作)
|
||||
* @api {post} api/project/adddeputy 任命项目管理员(仅负责人可操作)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@ -707,7 +707,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/project/deldeputy 罢免副负责人(仅主负责人可操作)
|
||||
* @api {post} api/project/deldeputy 罢免项目管理员(仅负责人可操作)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@ -715,7 +715,7 @@ class ProjectController extends AbstractController
|
||||
* @apiName deldeputy
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {Number} userid 要罢免的副负责人 userid
|
||||
* @apiParam {Number} userid 要罢免的项目管理员 userid
|
||||
*/
|
||||
public function deldeputy()
|
||||
{
|
||||
@ -1994,7 +1994,7 @@ class ProjectController extends AbstractController
|
||||
// 项目可见性
|
||||
$projectOwnerids = ProjectUser::whereProjectId($task->project_id)
|
||||
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
|
||||
->pluck('userid')->map(fn($v) => (int)$v)->toArray(); // 项目负责人(主+副)
|
||||
->pluck('userid')->map(fn($v) => (int)$v)->toArray(); // 项目负责人(含项目管理员)
|
||||
if ($task->visibility != 1 && !in_array($user->userid, $projectOwnerids)) {
|
||||
$taskUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); //任务负责人、协助人
|
||||
$subTaskUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); //子任务负责人、协助人
|
||||
@ -2504,7 +2504,7 @@ class ProjectController extends AbstractController
|
||||
} else {
|
||||
$projectOwner = ProjectUser::whereProjectId($task->project_id)
|
||||
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
|
||||
->pluck('userid')->toArray(); // 项目负责人(主+副)
|
||||
->pluck('userid')->toArray(); // 项目负责人(含项目管理员)
|
||||
$taskOwnerAndAssists = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->pluck('userid')->toArray();
|
||||
$visibleIds = array_merge($projectOwner, $taskOwnerAndAssists);
|
||||
$data['is_visible'] = in_array($user->userid, $visibleIds) ? 1 : 0;
|
||||
|
||||
@ -2146,7 +2146,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/department/adddeputy 任命副负责人(限管理员)
|
||||
* @api {post} api/users/department/adddeputy 任命部门管理员(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@ -2154,7 +2154,7 @@ class UsersController extends AbstractController
|
||||
* @apiName department__adddeputy
|
||||
*
|
||||
* @apiParam {Number} id 部门 id
|
||||
* @apiParam {Number} userid 副负责人 userid
|
||||
* @apiParam {Number} userid 部门管理员 userid
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@ -2178,7 +2178,7 @@ class UsersController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/users/department/deldeputy 罢免副负责人(限管理员)
|
||||
* @api {post} api/users/department/deldeputy 罢免部门管理员(限管理员)
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@ -2186,7 +2186,7 @@ class UsersController extends AbstractController
|
||||
* @apiName department__deldeputy
|
||||
*
|
||||
* @apiParam {Number} id 部门 id
|
||||
* @apiParam {Number} userid 要罢免的副负责人 userid
|
||||
* @apiParam {Number} userid 要罢免的部门管理员 userid
|
||||
*/
|
||||
public function department__deldeputy()
|
||||
{
|
||||
|
||||
@ -94,7 +94,7 @@ class Project extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 副负责人 userid 列表
|
||||
* 项目管理员 userid 列表
|
||||
* @return array
|
||||
*/
|
||||
public function getDeputyUseridsAttribute(): array
|
||||
@ -110,7 +110,7 @@ class Project extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否主负责人(与 project_users.owner=1 一致)
|
||||
* 是否项目负责人(与 project_users.owner=1 一致)
|
||||
*/
|
||||
public function isPrimaryOwner($userid): bool
|
||||
{
|
||||
@ -124,7 +124,7 @@ class Project extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否副负责人(与 project_users.owner=2 一致)
|
||||
* 是否项目管理员(与 project_users.owner=2 一致)
|
||||
*/
|
||||
public function isDeputyOwner($userid): bool
|
||||
{
|
||||
@ -138,7 +138,7 @@ class Project extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否负责人(主或副)
|
||||
* 是否负责人(含项目管理员)
|
||||
*/
|
||||
public function isOwner($userid): bool
|
||||
{
|
||||
@ -693,8 +693,8 @@ class Project extends AbstractModel
|
||||
* 获取项目信息(用于判断会员是否存在项目内)
|
||||
* @param int $project_id
|
||||
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
|
||||
* @param null|bool|string $mustOwner true:主或副都可(主+副共享操作);
|
||||
* 'primary':仅主(转让/删除/任命副等主独占操作);
|
||||
* @param null|bool|string $mustOwner true:负责人或项目管理员都可(共享操作);
|
||||
* 'primary':仅负责人(转让/删除/任命项目管理员等独占操作);
|
||||
* false:仅限非负责人;null:不限制
|
||||
* @return self
|
||||
*/
|
||||
|
||||
@ -1993,7 +1993,7 @@ class ProjectTask extends AbstractModel
|
||||
//
|
||||
$projectOwnerids = ProjectUser::whereProjectId($this->project_id)
|
||||
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
|
||||
->pluck('userid')->toArray(); // 项目负责人(主+副)
|
||||
->pluck('userid')->toArray(); // 项目负责人(含项目管理员)
|
||||
//
|
||||
$array = [];
|
||||
if (empty($userids)) {
|
||||
|
||||
@ -39,13 +39,13 @@ class ProjectUser extends AbstractModel
|
||||
{
|
||||
/** @var int 普通成员编码 */
|
||||
const OWNER_MEMBER = 0;
|
||||
/** @var int 主负责人编码 */
|
||||
/** @var int 项目负责人编码 */
|
||||
const OWNER_PRIMARY = 1;
|
||||
/** @var int 副负责人编码 */
|
||||
/** @var int 项目管理员编码 */
|
||||
const OWNER_DEPUTY = 2;
|
||||
|
||||
/**
|
||||
* 是否主负责人(owner=1)
|
||||
* 是否项目负责人(owner=1)
|
||||
*/
|
||||
public function isPrimaryOwner(): bool
|
||||
{
|
||||
@ -53,7 +53,7 @@ class ProjectUser extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否副负责人(owner=2)
|
||||
* 是否项目管理员(owner=2)
|
||||
*/
|
||||
public function isDeputyOwner(): bool
|
||||
{
|
||||
@ -61,7 +61,7 @@ class ProjectUser extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否负责人(主或副)
|
||||
* 是否负责人(含项目管理员)
|
||||
*/
|
||||
public function isOwner(): bool
|
||||
{
|
||||
@ -91,8 +91,8 @@ class ProjectUser extends AbstractModel
|
||||
foreach ($list as $item) {
|
||||
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
|
||||
if ($row) {
|
||||
// 已存在:仅当离职用户是主(owner=1)时把接收人升为主;
|
||||
// 离职用户是副(owner=2)时不传副给接收人(spec:副不替补)
|
||||
// 已存在:仅当离职用户是项目负责人(owner=1)时把接收人升为项目负责人;
|
||||
// 离职用户是项目管理员(owner=2)时不传项目管理员身份给接收人(spec:项目管理员不替补)
|
||||
if ((int)$item->owner === self::OWNER_PRIMARY) {
|
||||
$row->owner = self::OWNER_PRIMARY;
|
||||
}
|
||||
@ -100,7 +100,7 @@ class ProjectUser extends AbstractModel
|
||||
$row->save();
|
||||
$item->delete();
|
||||
} else {
|
||||
// 不存在:转移时如果离职用户是副,降级为普通成员(不带副身份过户给接收人)
|
||||
// 不存在:转移时如果离职用户是项目管理员,降级为普通成员(不带项目管理员身份过户给接收人)
|
||||
if ((int)$item->owner === self::OWNER_DEPUTY) {
|
||||
$item->owner = self::OWNER_MEMBER;
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 副负责人 userid 列表
|
||||
* 部门管理员 userid 列表
|
||||
* @return array
|
||||
*/
|
||||
public function getDeputyUseridsAttribute(): array
|
||||
@ -71,7 +71,7 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否主负责人(与 owner_userid 一致)
|
||||
* 是否部门负责人(与 owner_userid 一致)
|
||||
*/
|
||||
public function isPrimaryOwner($userid): bool
|
||||
{
|
||||
@ -82,7 +82,7 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否副负责人(在 user_department_owners 表里)
|
||||
* 是否部门管理员(在 user_department_owners 表里)
|
||||
*/
|
||||
public function isDeputyOwner($userid): bool
|
||||
{
|
||||
@ -96,7 +96,7 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否负责人(主或副)
|
||||
* 是否负责人(含部门管理员)
|
||||
*/
|
||||
public function isOwner($userid): bool
|
||||
{
|
||||
@ -127,7 +127,7 @@ class UserDepartment extends AbstractModel
|
||||
$dialog->owner_id = $this->owner_userid;
|
||||
if ($dialog->save()) {
|
||||
$dialog->joinGroup($this->owner_userid, 0, true);
|
||||
// 同步 role:原主 role=0、新主 role=1(副 role=2 保留不动)
|
||||
// 同步 role:原负责人 role=0、新负责人 role=1(部门管理员 role=2 保留不动)
|
||||
if ($oldOwnerId > 0 && $oldOwnerId !== (int)$this->owner_userid) {
|
||||
WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $oldOwnerId)
|
||||
@ -156,9 +156,9 @@ class UserDepartment extends AbstractModel
|
||||
$dialog->group_type = 'department';
|
||||
if ($dialog->save()) {
|
||||
$dialog->joinGroup($this->owner_userid, 0, true);
|
||||
// 同步 role:原主 role=0、新主 role=1、原副 role=0
|
||||
// 原副清零:避免 dialog_users.role=2 与 user_department_owners 不一致
|
||||
// (副关系不带过来,须通过 addDeputy 显式重新任命)
|
||||
// 同步 role:原负责人 role=0、新负责人 role=1、原部门管理员 role=0
|
||||
// 原部门管理员清零:避免 dialog_users.role=2 与 user_department_owners 不一致
|
||||
// (部门管理员关系不带过来,须通过 addDeputy 显式重新任命)
|
||||
if ($oldOwnerId > 0 && $oldOwnerId !== (int)$this->owner_userid) {
|
||||
WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $oldOwnerId)
|
||||
@ -214,10 +214,10 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 任命副负责人
|
||||
* - 副自动加入 users.department(成为部门成员,与主对齐)
|
||||
* - 副自动加入部门群 + 设 role=2
|
||||
* - 幂等(已是副不报错)
|
||||
* 任命部门管理员
|
||||
* - 部门管理员自动加入 users.department(成为部门成员,与负责人对齐)
|
||||
* - 部门管理员自动加入部门群 + 设 role=2
|
||||
* - 幂等(已是部门管理员不报错)
|
||||
*
|
||||
* @param int $userid
|
||||
* @return void
|
||||
@ -237,13 +237,13 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
|
||||
AbstractModel::transaction(function () use ($userid, $user) {
|
||||
// 写副表(unique key 自动幂等)
|
||||
// 写部门管理员表(unique key 自动幂等)
|
||||
\DB::table('user_department_owners')->insertOrIgnore([
|
||||
'department_id' => $this->id,
|
||||
'userid' => $userid,
|
||||
]);
|
||||
|
||||
// 加入 users.department(成为部门成员,与主对齐)
|
||||
// 加入 users.department(成为部门成员,与负责人对齐)
|
||||
$userDeptIds = $user->department; // accessor 返回数组
|
||||
if (!in_array($this->id, $userDeptIds)) {
|
||||
$userDeptIds = array_merge($userDeptIds, [$this->id]);
|
||||
@ -251,7 +251,7 @@ class UserDepartment extends AbstractModel
|
||||
$user->save();
|
||||
}
|
||||
|
||||
// 加副入部门群 + 设 role=2
|
||||
// 加部门管理员入部门群 + 设 role=2
|
||||
if ($this->dialog_id > 0) {
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
if ($dialog) {
|
||||
@ -270,9 +270,9 @@ class UserDepartment extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 罢免副负责人
|
||||
* - 删副表记录
|
||||
* - 从 users.department 移除该部门 ID(与主"离开部门"对齐)
|
||||
* 罢免部门管理员
|
||||
* - 删部门管理员表记录
|
||||
* - 从 users.department 移除该部门 ID(与负责人"离开部门"对齐)
|
||||
* - 退出部门群(成员关系=群关系一致)
|
||||
* - 幂等
|
||||
*
|
||||
@ -341,7 +341,7 @@ class UserDepartment extends AbstractModel
|
||||
// 解散群组
|
||||
$dialog = WebSocketDialog::find($this->dialog_id);
|
||||
$dialog?->deleteDialog();
|
||||
// 清理副负责人记录(防悬挂)
|
||||
// 清理部门管理员记录(防悬挂)
|
||||
\DB::table('user_department_owners')->where('department_id', $this->id)->delete();
|
||||
//
|
||||
$this->delete();
|
||||
@ -355,7 +355,7 @@ class UserDepartment extends AbstractModel
|
||||
*/
|
||||
public static function transfer($originalUserid, $newUserid)
|
||||
{
|
||||
// 主转让(保持现有逻辑)
|
||||
// 部门负责人转让(保持现有逻辑)
|
||||
self::whereOwnerUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
|
||||
/** @var self $item */
|
||||
foreach ($list as $item) {
|
||||
@ -364,7 +364,7 @@ class UserDepartment extends AbstractModel
|
||||
]);
|
||||
}
|
||||
});
|
||||
// 副离职清理(新增):直接删除离职用户的所有副记录
|
||||
// 部门管理员离职清理(新增):直接删除离职用户的所有部门管理员记录
|
||||
// 不需要清群 role —— UserTransfer::exitDialog 会把人踢出所有群,role 随成员关系一起消失
|
||||
\DB::table('user_department_owners')
|
||||
->where('userid', $originalUserid)
|
||||
|
||||
@ -520,7 +520,7 @@ class WebSocketDialog extends AbstractModel
|
||||
foreach ($list as $item) {
|
||||
if ($checkDelete) {
|
||||
if ($type === 'remove') {
|
||||
// 移出时:如果是全员群仅允许管理员操作,其他群主/副群主/邀请人可以操作
|
||||
// 移出时:如果是全员群仅允许管理员操作,其他群主/群管理员/邀请人可以操作
|
||||
if ($this->group_type === 'all') {
|
||||
User::auth("admin");
|
||||
} else {
|
||||
@ -529,12 +529,12 @@ class WebSocketDialog extends AbstractModel
|
||||
if ($actor <= 0) {
|
||||
throw new ApiException('只有群主或邀请人可以移出成员');
|
||||
}
|
||||
// 主群主、副群主、邀请人可移出
|
||||
// 群主、群管理员、邀请人可移出
|
||||
$allowedActor = $this->isOwner($actor) || $actor === (int)$item->inviter;
|
||||
if (!$allowedActor) {
|
||||
throw new ApiException('只有群主或邀请人可以移出成员');
|
||||
}
|
||||
// 副群主不能移出主群主或其他副群主
|
||||
// 群管理员不能移出群主或其他群管理员
|
||||
if ($this->isDeputyOwner($actor)) {
|
||||
$targetIsOwner = $this->isPrimaryOwner($item->userid) || $this->isDeputyOwner($item->userid);
|
||||
if ($targetIsOwner) {
|
||||
@ -659,7 +659,7 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否主群主(与 owner_id 一致)
|
||||
* 是否群主(与 owner_id 一致)
|
||||
*/
|
||||
public function isPrimaryOwner($userid): bool
|
||||
{
|
||||
@ -667,7 +667,7 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否副群主(仅 web_socket_dialog_users.role=2)
|
||||
* 是否群管理员(仅 web_socket_dialog_users.role=2)
|
||||
*/
|
||||
public function isDeputyOwner($userid): bool
|
||||
{
|
||||
@ -681,7 +681,7 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否群主(主或副)
|
||||
* 是否群主(含群管理员)
|
||||
*/
|
||||
public function isOwner($userid): bool
|
||||
{
|
||||
@ -689,7 +689,7 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
|
||||
/**
|
||||
* 副群主 userid 列表
|
||||
* 群管理员 userid 列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
|
||||
@ -16,7 +16,7 @@ class AddRoleToWebSocketDialogUsers extends Migration
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_users', 'role')) {
|
||||
$table->tinyInteger('role')->default(0)->after('userid')
|
||||
->comment('0=普通成员 1=主群主 2=副群主');
|
||||
->comment('0=普通成员 1=群主 2=群管理员');
|
||||
$table->index(['dialog_id', 'role'], 'idx_dialog_role');
|
||||
}
|
||||
});
|
||||
|
||||
@ -17,7 +17,7 @@ class CreateUserDepartmentOwnersTable extends Migration
|
||||
Schema::create('user_department_owners', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('department_id')->comment('部门ID');
|
||||
$table->unsignedBigInteger('userid')->comment('副负责人 userid');
|
||||
$table->unsignedBigInteger('userid')->comment('部门管理员 userid');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->unique(['department_id', 'userid'], 'uniq_dept_user');
|
||||
$table->index('userid', 'idx_userid');
|
||||
|
||||
@ -774,7 +774,7 @@ export default {
|
||||
},
|
||||
|
||||
deputyWaitDemote() {
|
||||
// 所有从副负责人列表中移出的人(即使同时被踢出项目,也在罢免段显示,避免操作隐身)
|
||||
// 所有从项目管理员列表中移出的人(即使同时被踢出项目,也在罢免段显示,避免操作隐身)
|
||||
const {deputy_userids = [], deputy_useridbak = []} = this.userData;
|
||||
return deputy_useridbak.filter(id => !deputy_userids.includes(id));
|
||||
},
|
||||
@ -822,7 +822,7 @@ export default {
|
||||
},
|
||||
|
||||
memberRowUncancelable() {
|
||||
// 项目成员行:主+当前副选择(响应式)都不可移除
|
||||
// 项目成员行:负责人 + 当前项目管理员选择(响应式)都不可移除
|
||||
if (!this.projectData) return [];
|
||||
const deputies = (this.userData && Array.isArray(this.userData.deputy_userids))
|
||||
? this.userData.deputy_userids
|
||||
@ -834,13 +834,13 @@ export default {
|
||||
},
|
||||
|
||||
deputyRowUncancelable() {
|
||||
// 副负责人行:防御性锁定主(理论上主不会出现在该 v-model 里)
|
||||
// 项目管理员行:防御性锁定负责人(理论上负责人不会出现在该 v-model 里)
|
||||
if (!this.projectData) return [];
|
||||
return [this.projectData.owner_userid];
|
||||
},
|
||||
|
||||
deputyRowDisabledChoice() {
|
||||
// 副负责人候选:排除主(不能任命主为副)
|
||||
// 项目管理员候选:排除负责人(不能任命负责人为项目管理员)
|
||||
if (!this.projectData) return [];
|
||||
return [this.projectData.owner_userid];
|
||||
},
|
||||
@ -1169,7 +1169,7 @@ export default {
|
||||
this.handleColumnDebounce(100);
|
||||
},
|
||||
'userData.deputy_userids'(newDeputies) {
|
||||
// 副负责人必须是项目成员:副行新增时自动并入成员行(罢免时不联动移除)
|
||||
// 项目管理员必须是项目成员:项目管理员行新增时自动并入成员行(罢免时不联动移除)
|
||||
if (!Array.isArray(newDeputies) || !Array.isArray(this.userData.userids)) {
|
||||
return;
|
||||
}
|
||||
@ -1507,7 +1507,7 @@ export default {
|
||||
|
||||
onUser() {
|
||||
this.userLoad++;
|
||||
// 副负责人必须是项目成员:把 deputy 并入 userid 列表(前端归一化)
|
||||
// 项目管理员必须是项目成员:把 deputy 并入 userid 列表(前端归一化)
|
||||
const baseUserids = (this.userData.userids || []).slice();
|
||||
const deputyUserids = (this.userData.deputy_userids || []).slice();
|
||||
const mergedUserids = Array.from(new Set([...baseUserids, ...deputyUserids]));
|
||||
@ -1516,7 +1516,7 @@ export default {
|
||||
project_id: this.projectId,
|
||||
userid: mergedUserids,
|
||||
};
|
||||
// 仅主负责人发送 deputy_userid;副负责人/其他角色不发送(后端也会忽略)
|
||||
// 仅项目负责人发送 deputy_userid;项目管理员/其他角色不发送(后端也会忽略)
|
||||
if (this.canManageDeputy) {
|
||||
payload.deputy_userid = deputyUserids;
|
||||
}
|
||||
|
||||
@ -833,7 +833,7 @@ export default {
|
||||
},
|
||||
|
||||
deputyDisabledChoice() {
|
||||
// 主负责人不能同时是副;已是副的不需要再选
|
||||
// 部门负责人不能同时是部门管理员;已是部门管理员的不需要再选
|
||||
return [
|
||||
...(this.departmentData.owner_userid || []),
|
||||
];
|
||||
@ -1088,8 +1088,8 @@ export default {
|
||||
});
|
||||
$A.messageSuccess(res.msg);
|
||||
|
||||
// 副列表同步(编辑/新建都支持)
|
||||
// 编辑场景:从 departmentList 取旧副;新建场景:从刚返回的列表反查刚创建的部门
|
||||
// 部门管理员列表同步(编辑/新建都支持)
|
||||
// 编辑场景:从 departmentList 取旧部门管理员;新建场景:从刚返回的列表反查刚创建的部门
|
||||
let targetId = this.departmentData.id;
|
||||
let oldDeputies = [];
|
||||
if (targetId > 0) {
|
||||
|
||||
@ -122,7 +122,7 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
$member = $this->makeUser('d3_m@test.local');
|
||||
$dept = $this->makeDepartment($owner->userid);
|
||||
|
||||
// 手动插入副记录(addDeputy 在 Task 5 才实现)
|
||||
// 手动插入部门管理员记录(addDeputy 在 Task 5 才实现)
|
||||
DB::table('user_department_owners')->insert([
|
||||
'department_id' => $dept->id,
|
||||
'userid' => $deputy->userid,
|
||||
@ -184,7 +184,7 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
$deputy = $this->makeUser('d4b_dep@test.local');
|
||||
$dept = $this->makeDepartment($oldOwner->userid);
|
||||
|
||||
// 加 deputy 入群 + 副记录 + role=2(pushMsg=false 跳过 Swoole)
|
||||
// 加 deputy 入群 + 部门管理员记录 + role=2(pushMsg=false 跳过 Swoole)
|
||||
$dialog = WebSocketDialog::find($dept->dialog_id);
|
||||
$dialog->joinGroup($deputy->userid, 0, null, false);
|
||||
DB::table('user_department_owners')->insert([
|
||||
@ -204,9 +204,9 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
'owner_userid' => $newOwner->userid,
|
||||
]);
|
||||
|
||||
// 副表保留
|
||||
// 部门管理员表保留
|
||||
$this->assertContains($deputy->userid, $dept->fresh()->deputy_userids);
|
||||
// 副 role 保留
|
||||
// 部门管理员 role 保留
|
||||
$depRole = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||||
->where('userid', $deputy->userid)->value('role');
|
||||
$this->assertEquals(2, (int)$depRole);
|
||||
@ -222,11 +222,11 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
|
||||
$dept = $dept->fresh();
|
||||
$this->assertContains($deputy->userid, $dept->deputy_userids);
|
||||
// 副已入群
|
||||
// 部门管理员已入群
|
||||
$exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||||
->where('userid', $deputy->userid)->exists();
|
||||
$this->assertTrue($exists);
|
||||
// 副 role=2
|
||||
// 部门管理员 role=2
|
||||
$role = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||||
->where('userid', $deputy->userid)->value('role');
|
||||
$this->assertEquals(2, (int)$role);
|
||||
@ -272,23 +272,23 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
$dept = $this->makeDepartment($owner->userid);
|
||||
$this->simulateAddDeputy($dept, $deputy->userid);
|
||||
|
||||
// 任命后副应该入 users.department 并加入部门群
|
||||
$this->assertContains($dept->id, User::find($deputy->userid)->department, '任命副后应加入 users.department');
|
||||
// 任命后部门管理员应该入 users.department 并加入部门群
|
||||
$this->assertContains($dept->id, User::find($deputy->userid)->department, '任命部门管理员后应加入 users.department');
|
||||
$this->assertTrue(
|
||||
WebSocketDialogUser::where('dialog_id', $dept->dialog_id)->where('userid', $deputy->userid)->exists(),
|
||||
'任命副后应加入部门群'
|
||||
'任命部门管理员后应加入部门群'
|
||||
);
|
||||
|
||||
$this->simulateDelDeputy($dept, $deputy->userid);
|
||||
|
||||
$dept = $dept->fresh();
|
||||
$this->assertNotContains($deputy->userid, $dept->deputy_userids);
|
||||
// 罢免后从 users.department 移除(与主"离开部门"对齐)
|
||||
$this->assertNotContains($dept->id, User::find($deputy->userid)->department, '罢免副后应从 users.department 移除');
|
||||
// 罢免后从 users.department 移除(与负责人"离开部门"对齐)
|
||||
$this->assertNotContains($dept->id, User::find($deputy->userid)->department, '罢免部门管理员后应从 users.department 移除');
|
||||
// 退出部门群(成员关系=群关系一致)
|
||||
$exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
|
||||
->where('userid', $deputy->userid)->exists();
|
||||
$this->assertFalse($exists, '罢免副后应退出部门群(成员关系=群关系)');
|
||||
$this->assertFalse($exists, '罢免部门管理员后应退出部门群(成员关系=群关系)');
|
||||
}
|
||||
|
||||
public function test_delDeputy_idempotent_for_non_deputy()
|
||||
@ -297,7 +297,7 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
$member = $this->makeUser('d6b_m@test.local');
|
||||
$dept = $this->makeDepartment($owner->userid);
|
||||
|
||||
// member 不是副,调 delDeputy 不应抛错
|
||||
// member 不是部门管理员,调 delDeputy 不应抛错
|
||||
$this->simulateDelDeputy($dept, $member->userid);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
@ -326,15 +326,15 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
|
||||
UserDepartment::transfer($departing->userid, $receiver->userid);
|
||||
|
||||
// 离职的副记录已删
|
||||
// 离职的部门管理员记录已删
|
||||
$this->assertNotContains($departing->userid, $dept->fresh()->deputy_userids);
|
||||
// receiver 没有继承副身份
|
||||
// receiver 没有继承部门管理员身份
|
||||
$this->assertNotContains($receiver->userid, $dept->fresh()->deputy_userids);
|
||||
}
|
||||
|
||||
public function test_user_transfer_inherits_departing_primary()
|
||||
{
|
||||
// 主转让仍要把主权位传给接收人(保留现有行为)
|
||||
// 部门负责人转让仍要把负责人身份传给接收人(保留现有行为)
|
||||
$departing = $this->makeUser('d7c_dep@test.local');
|
||||
$receiver = $this->makeUser('d7c_rec@test.local');
|
||||
$dept = $this->makeDepartment($departing->userid);
|
||||
@ -346,7 +346,7 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
|
||||
public function test_deleteDepartment_recursively_cleans_child_deputies()
|
||||
{
|
||||
// 父部门 + 子部门各有副,删父部门时副记录应级联清理
|
||||
// 父部门 + 子部门各有部门管理员,删父部门时部门管理员记录应级联清理
|
||||
$owner = $this->makeUser('d7d_o@test.local');
|
||||
$deputyParent = $this->makeUser('d7d_dp@test.local');
|
||||
$deputyChild = $this->makeUser('d7d_dc@test.local');
|
||||
@ -365,7 +365,7 @@ class MultiOwnerDepartmentTest extends TestCase
|
||||
|
||||
$parentId = $parent->id;
|
||||
$childId = $child->id;
|
||||
$parent->deleteDepartment(); // 递归删子部门 + 清各自副
|
||||
$parent->deleteDepartment(); // 递归删子部门 + 清各自部门管理员
|
||||
|
||||
$this->assertEquals(0, DB::table('user_department_owners')->where('department_id', $parentId)->count());
|
||||
$this->assertEquals(0, DB::table('user_department_owners')->where('department_id', $childId)->count());
|
||||
|
||||
@ -48,7 +48,7 @@ class MultiOwnerGroupTest extends TestCase
|
||||
$member = $this->makeUser('m2@test.local');
|
||||
$dialog = $this->makeGroup($owner->userid, [$deputy->userid, $member->userid]);
|
||||
|
||||
// 手工把 deputy 设为副群主
|
||||
// 手工把 deputy 设为群管理员
|
||||
WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $deputy->userid)
|
||||
->update(['role' => 2]);
|
||||
@ -111,13 +111,13 @@ class MultiOwnerGroupTest extends TestCase
|
||||
->where('userid', $deputy->userid)
|
||||
->update(['role' => 2]);
|
||||
|
||||
// 模拟副群主退群(pushMsg=false 跳过 Swoole 推送,仅验证 DB 状态)
|
||||
// 模拟群管理员退群(pushMsg=false 跳过 Swoole 推送,仅验证 DB 状态)
|
||||
$dialog->exitGroup($deputy->userid, 'exit', false, false);
|
||||
|
||||
$exists = WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $deputy->userid)
|
||||
->exists();
|
||||
$this->assertFalse($exists, '退群后副群主记录应被删除');
|
||||
$this->assertFalse($exists, '退群后群管理员记录应被删除');
|
||||
$this->assertNotContains($deputy->userid, $dialog->fresh()->deputy_ids);
|
||||
}
|
||||
|
||||
@ -358,13 +358,13 @@ class MultiOwnerGroupTest extends TestCase
|
||||
return ['allowed' => false, 'error' => '目标用户不在群内'];
|
||||
}
|
||||
|
||||
// 主群主、副群主、邀请人可移出
|
||||
// 群主、群管理员、邀请人可移出
|
||||
$allowedActor = $dialog->isOwner($actorId) || $actorId === (int)$item->inviter;
|
||||
if (!$allowedActor) {
|
||||
return ['allowed' => false, 'error' => '只有群主或邀请人可以移出成员'];
|
||||
}
|
||||
|
||||
// 副群主不能移出主群主或其他副群主
|
||||
// 群管理员不能移出群主或其他群管理员
|
||||
if ($dialog->isDeputyOwner($actorId)) {
|
||||
$targetIsOwner = $dialog->isPrimaryOwner($targetId) || $dialog->isDeputyOwner($targetId);
|
||||
if ($targetIsOwner) {
|
||||
@ -389,9 +389,9 @@ class MultiOwnerGroupTest extends TestCase
|
||||
WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $deputy->userid)->update(['role' => 2]);
|
||||
|
||||
// 验证权限逻辑:副群主可移出普通成员
|
||||
// 验证权限逻辑:群管理员可移出普通成员
|
||||
$result = $this->simulateRemovePermission($dialog, $deputy->userid, $member->userid);
|
||||
$this->assertTrue($result['allowed'], '副群主应能移出普通成员,错误:' . ($result['error'] ?? ''));
|
||||
$this->assertTrue($result['allowed'], '群管理员应能移出普通成员,错误:' . ($result['error'] ?? ''));
|
||||
|
||||
// 验证实际移出操作(checkDelete=false 绕过 auth,直接测试 DB 状态)
|
||||
$dialog->exitGroup($member->userid, 'remove', false, false);
|
||||
@ -408,9 +408,9 @@ class MultiOwnerGroupTest extends TestCase
|
||||
WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $deputy->userid)->update(['role' => 2]);
|
||||
|
||||
// 验证权限逻辑:副群主不可移出主群主
|
||||
// 验证权限逻辑:群管理员不可移出群主
|
||||
$result = $this->simulateRemovePermission($dialog, $deputy->userid, $owner->userid);
|
||||
$this->assertFalse($result['allowed'], '副群主不应能移出主群主');
|
||||
$this->assertFalse($result['allowed'], '群管理员不应能移出群主');
|
||||
$this->assertNotNull($result['error']);
|
||||
}
|
||||
|
||||
@ -424,9 +424,9 @@ class MultiOwnerGroupTest extends TestCase
|
||||
->whereIn('userid', [$deputy1->userid, $deputy2->userid])
|
||||
->update(['role' => 2]);
|
||||
|
||||
// 验证权限逻辑:副群主不可移出其他副群主
|
||||
// 验证权限逻辑:群管理员不可移出其他群管理员
|
||||
$result = $this->simulateRemovePermission($dialog, $deputy1->userid, $deputy2->userid);
|
||||
$this->assertFalse($result['allowed'], '副群主不应能移出其他副群主');
|
||||
$this->assertFalse($result['allowed'], '群管理员不应能移出其他群管理员');
|
||||
$this->assertEquals('群管理员不能移出群主或其他群管理员', $result['error']);
|
||||
}
|
||||
|
||||
@ -466,11 +466,11 @@ class MultiOwnerGroupTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证离职移交时副群主角色被正确清除。
|
||||
* 验证离职移交时群管理员角色被正确清除。
|
||||
*
|
||||
* UserTransfer::exitDialog() 对离职用户调用 exitGroup($original_userid, 'remove', false, false),
|
||||
* exitGroup 内部直接 hard-delete web_socket_dialog_users 记录($item->delete()),
|
||||
* 因此副群主的 role 随记录一起消失,无需额外逻辑。
|
||||
* 因此群管理员的 role 随记录一起消失,无需额外逻辑。
|
||||
*
|
||||
* 本测试直接调用 exitDialog()(通过 UserTransfer 实例),绕过 start() 中的项目/任务/文件迁移,
|
||||
* 以确保在无 Swoole 推送的 PHPUnit 环境中可以正常运行。
|
||||
@ -482,13 +482,13 @@ class MultiOwnerGroupTest extends TestCase
|
||||
$receiver = $this->makeUser('rec11@test.local');
|
||||
$dialog = $this->makeGroup($owner->userid, [$departing->userid, $receiver->userid]);
|
||||
|
||||
// 将离职用户设为副群主
|
||||
// 将离职用户设为群管理员
|
||||
WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $departing->userid)
|
||||
->update(['role' => 2]);
|
||||
|
||||
// 验证前置条件
|
||||
$this->assertContains($departing->userid, $dialog->fresh()->deputy_ids, '前置条件:离职用户应是副群主');
|
||||
$this->assertContains($departing->userid, $dialog->fresh()->deputy_ids, '前置条件:离职用户应是群管理员');
|
||||
|
||||
// 通过 UserTransfer 触发 exitDialog(使用正确字段名 original_userid / new_userid)
|
||||
$transfer = \App\Models\UserTransfer::createInstance([
|
||||
@ -500,14 +500,14 @@ class MultiOwnerGroupTest extends TestCase
|
||||
|
||||
$freshDialog = $dialog->fresh();
|
||||
|
||||
// 离职用户不应再出现在副群主列表中
|
||||
$this->assertNotContains($departing->userid, $freshDialog->deputy_ids, '离职用户不应留在副群主列表');
|
||||
// 离职用户不应再出现在群管理员列表中
|
||||
$this->assertNotContains($departing->userid, $freshDialog->deputy_ids, '离职用户不应留在群管理员列表');
|
||||
// 离职用户的成员记录应已删除
|
||||
$exists = WebSocketDialogUser::where('dialog_id', $dialog->id)
|
||||
->where('userid', $departing->userid)
|
||||
->exists();
|
||||
$this->assertFalse($exists, 'exitDialog 后离职用户的成员记录应被删除');
|
||||
// 接收方不应自动继承副群主角色
|
||||
$this->assertNotContains($receiver->userid, $freshDialog->deputy_ids, '接收方不应自动继承副群主角色');
|
||||
// 接收方不应自动继承群管理员角色
|
||||
$this->assertNotContains($receiver->userid, $freshDialog->deputy_ids, '接收方不应自动继承群管理员角色');
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,17 +117,17 @@ class MultiOwnerProjectTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟合并后的 ProjectController::user() 端点:同步成员 + 副负责人。
|
||||
* 模拟合并后的 ProjectController::user() 端点:同步成员 + 项目管理员。
|
||||
*
|
||||
* @param Project $project 项目实例
|
||||
* @param int $callerUserid 调用方 userid(用于权限判断)
|
||||
* @param int[] $userids 最终成员完整列表(必须包含主负责人)
|
||||
* @param int[]|null $deputyUserids 最终副负责人完整列表;null 表示不设置(沿用既有副)
|
||||
* @param int[] $userids 最终成员完整列表(必须包含项目负责人)
|
||||
* @param int[]|null $deputyUserids 最终项目管理员完整列表;null 表示不设置(沿用既有项目管理员)
|
||||
* @return int[] 被移除的成员 userids
|
||||
*/
|
||||
private function simulateMemberSync(Project $project, int $callerUserid, array $userids, ?array $deputyUserids): array
|
||||
{
|
||||
// 鉴权:调用方必须是主或副
|
||||
// 鉴权:调用方必须是项目负责人或项目管理员
|
||||
$callerRow = ProjectUser::where('project_id', $project->id)
|
||||
->where('userid', $callerUserid)->first();
|
||||
if (!$callerRow || !in_array((int)$callerRow->owner, [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY], true)) {
|
||||
@ -474,12 +474,12 @@ class MultiOwnerProjectTest extends TestCase
|
||||
|
||||
\App\Models\ProjectUser::transfer($departing->userid, $receiver->userid);
|
||||
|
||||
// 离职的副已不在 project_users
|
||||
// 离职的项目管理员已不在 project_users
|
||||
$this->assertFalse(
|
||||
ProjectUser::where('project_id', $project->id)
|
||||
->where('userid', $departing->userid)->exists()
|
||||
);
|
||||
// receiver 没有继承副身份
|
||||
// receiver 没有继承项目管理员身份
|
||||
$row = ProjectUser::where('project_id', $project->id)->where('userid', $receiver->userid)->first();
|
||||
$this->assertNotEquals(2, (int)$row->owner);
|
||||
$this->assertNotContains($receiver->userid, $project->fresh()->deputy_userids);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user