feat(multi-owner): 群/项目/部门支持主+副双负责人体系

This commit is contained in:
kuaifan 2026-05-09 12:29:38 +00:00
parent 24710289e1
commit 64649b514e
15 changed files with 118 additions and 118 deletions

View File

@ -2897,7 +2897,7 @@ class DialogController extends AbstractController
} }
// //
$dialog = WebSocketDialog::checkDialog($dialog_id); $dialog = WebSocketDialog::checkDialog($dialog_id);
// 有群主(主或副)时,仅群主/副群主可邀请;无群主时,任意成员可邀请 // 有群主时,仅群主/群管理员可邀请;无群主时,任意成员可邀请
if ($dialog->owner_id > 0 && !$dialog->isOwner($user->userid)) { if ($dialog->owner_id > 0 && !$dialog->isOwner($user->userid)) {
throw new \App\Exceptions\ApiException('仅限群主或群管理员操作'); throw new \App\Exceptions\ApiException('仅限群主或群管理员操作');
} }
@ -3013,7 +3013,7 @@ class DialogController extends AbstractController
} }
/** /**
* 任命副群主(仅主群主可操作) * 任命群管理员(仅群主可操作)
* *
* @apiParam {Number} dialog_id 群对话ID * @apiParam {Number} dialog_id 群对话ID
* @apiParam {Number} userid 要任命的群成员 userid * @apiParam {Number} userid 要任命的群成员 userid
@ -3028,7 +3028,7 @@ class DialogController extends AbstractController
return Base::retError('请选择有效的成员'); return Base::retError('请选择有效的成员');
} }
$dialog = WebSocketDialog::checkDialog($dialog_id, true); // checkOwner=true群主 $dialog = WebSocketDialog::checkDialog($dialog_id, true); // checkOwner=true群主
$dialog->checkGroup('user'); // 仅普通群 $dialog->checkGroup('user'); // 仅普通群
$member = WebSocketDialogUser::where('dialog_id', $dialog->id) $member = WebSocketDialogUser::where('dialog_id', $dialog->id)
@ -3054,10 +3054,10 @@ class DialogController extends AbstractController
} }
/** /**
* 罢免副群主(仅主群主可操作) * 罢免群管理员(仅群主可操作)
* *
* @apiParam {Number} dialog_id 群对话ID * @apiParam {Number} dialog_id 群对话ID
* @apiParam {Number} userid 要罢免的副群主 userid * @apiParam {Number} userid 要罢免的群管理员 userid
*/ */
public function group__deldeputy() public function group__deldeputy()
{ {

View File

@ -381,7 +381,7 @@ class ProjectController extends AbstractController
* *
* @apiParam {Number} project_id 项目ID * @apiParam {Number} project_id 项目ID
* @apiParam {Number[]} userid 成员userid数组最终完整列表 * @apiParam {Number[]} userid 成员userid数组最终完整列表
* @apiParam {Number[]} [deputy_userid] 副负责人userid数组可选仅主负责人有效;必须是 userid 子集) * @apiParam {Number[]} [deputy_userid] 项目管理员userid数组可选负责人有效;必须是 userid 子集)
* *
* @apiSuccess {Number} ret 返回状态码1正确、0错误 * @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述) * @apiSuccess {String} msg 返回信息(错误描述)
@ -408,7 +408,7 @@ class ProjectController extends AbstractController
// //
$project = Project::userProject($project_id, true, true); $project = Project::userProject($project_id, true, true);
// //
// 仅主负责人可设置副负责人;副负责人/其他角色提交 deputy_userid 一律忽略 // 仅负责人可设置项目管理员;项目管理员/其他角色提交 deputy_userid 一律忽略
$isPrimary = (int)$project->owner === ProjectUser::OWNER_PRIMARY; $isPrimary = (int)$project->owner === ProjectUser::OWNER_PRIMARY;
$applyDeputy = $isPrimary && $deputy_userid !== null; $applyDeputy = $isPrimary && $deputy_userid !== null;
// //
@ -434,7 +434,7 @@ class ProjectController extends AbstractController
$row->exitProject(); $row->exitProject();
} }
// //
// 副负责人 diff仅主负责人有效) // 项目管理员 diff负责人有效)
if ($applyDeputy) { if ($applyDeputy) {
$currentDeputies = ProjectUser::whereProjectId($project->id) $currentDeputies = ProjectUser::whereProjectId($project->id)
->where('owner', ProjectUser::OWNER_DEPUTY) ->where('owner', ProjectUser::OWNER_DEPUTY)
@ -624,11 +624,11 @@ class ProjectController extends AbstractController
} }
// //
AbstractModel::transaction(function() use ($owner_userid, $project) { AbstractModel::transaction(function() use ($owner_userid, $project) {
// 仅清除原主 owner=1 owner=2 保留) // 仅清除原负责人 owner=1项目管理员 owner=2 保留)
ProjectUser::whereProjectId($project->id) ProjectUser::whereProjectId($project->id)
->whereOwner(ProjectUser::OWNER_PRIMARY) ->whereOwner(ProjectUser::OWNER_PRIMARY)
->change(['owner' => 0]); ->change(['owner' => 0]);
// 设新主 owner=1如新主原本是副,从 2 升为 1 // 设新负责人 owner=1如新负责人原本是项目管理员,从 2 升为 1
ProjectUser::updateInsert([ ProjectUser::updateInsert([
'project_id' => $project->id, 'project_id' => $project->id,
'userid' => $owner_userid, 'userid' => $owner_userid,
@ -648,7 +648,7 @@ class ProjectController extends AbstractController
$project->addLog("移交项目给", ['userid' => $owner_userid]); $project->addLog("移交项目给", ['userid' => $owner_userid]);
}); });
// //
// pushMsg 带 deputy_userids前端可直接更新列表无需重拉 // pushMsg 带 deputy_userids前端可直接更新项目管理员列表无需重拉
$project->pushMsg('detail', [ $project->pushMsg('detail', [
'owner_userid' => $project->fresh()->owner_userid, 'owner_userid' => $project->fresh()->owner_userid,
'deputy_userids' => $project->fresh()->deputy_userids, '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身份 * @apiDescription 需要token身份
* @apiVersion 1.0.0 * @apiVersion 1.0.0
@ -707,7 +707,7 @@ class ProjectController extends AbstractController
} }
/** /**
* @api {post} api/project/deldeputy 罢免副负责人(仅主负责人可操作) * @api {post} api/project/deldeputy 罢免项目管理员(仅负责人可操作)
* *
* @apiDescription 需要token身份 * @apiDescription 需要token身份
* @apiVersion 1.0.0 * @apiVersion 1.0.0
@ -715,7 +715,7 @@ class ProjectController extends AbstractController
* @apiName deldeputy * @apiName deldeputy
* *
* @apiParam {Number} project_id 项目ID * @apiParam {Number} project_id 项目ID
* @apiParam {Number} userid 要罢免的副负责人 userid * @apiParam {Number} userid 要罢免的项目管理员 userid
*/ */
public function deldeputy() public function deldeputy()
{ {
@ -1994,7 +1994,7 @@ class ProjectController extends AbstractController
// 项目可见性 // 项目可见性
$projectOwnerids = ProjectUser::whereProjectId($task->project_id) $projectOwnerids = ProjectUser::whereProjectId($task->project_id)
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY]) ->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)) { if ($task->visibility != 1 && !in_array($user->userid, $projectOwnerids)) {
$taskUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); //任务负责人、协助人 $taskUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); //任务负责人、协助人
$subTaskUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); //子任务负责人、协助人 $subTaskUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); //子任务负责人、协助人
@ -2504,7 +2504,7 @@ class ProjectController extends AbstractController
} else { } else {
$projectOwner = ProjectUser::whereProjectId($task->project_id) $projectOwner = ProjectUser::whereProjectId($task->project_id)
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY]) ->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(); $taskOwnerAndAssists = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->pluck('userid')->toArray();
$visibleIds = array_merge($projectOwner, $taskOwnerAndAssists); $visibleIds = array_merge($projectOwner, $taskOwnerAndAssists);
$data['is_visible'] = in_array($user->userid, $visibleIds) ? 1 : 0; $data['is_visible'] = in_array($user->userid, $visibleIds) ? 1 : 0;

View File

@ -2146,7 +2146,7 @@ class UsersController extends AbstractController
} }
/** /**
* @api {post} api/users/department/adddeputy 任命副负责人(限管理员) * @api {post} api/users/department/adddeputy 任命部门管理员(限管理员)
* *
* @apiDescription 需要token身份 * @apiDescription 需要token身份
* @apiVersion 1.0.0 * @apiVersion 1.0.0
@ -2154,7 +2154,7 @@ class UsersController extends AbstractController
* @apiName department__adddeputy * @apiName department__adddeputy
* *
* @apiParam {Number} id 部门 id * @apiParam {Number} id 部门 id
* @apiParam {Number} userid 副负责人 userid * @apiParam {Number} userid 部门管理员 userid
* *
* @apiSuccess {Number} ret 返回状态码1正确、0错误 * @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述) * @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身份 * @apiDescription 需要token身份
* @apiVersion 1.0.0 * @apiVersion 1.0.0
@ -2186,7 +2186,7 @@ class UsersController extends AbstractController
* @apiName department__deldeputy * @apiName department__deldeputy
* *
* @apiParam {Number} id 部门 id * @apiParam {Number} id 部门 id
* @apiParam {Number} userid 要罢免的副负责人 userid * @apiParam {Number} userid 要罢免的部门管理员 userid
*/ */
public function department__deldeputy() public function department__deldeputy()
{ {

View File

@ -94,7 +94,7 @@ class Project extends AbstractModel
} }
/** /**
* 副负责人 userid 列表 * 项目管理员 userid 列表
* @return array * @return array
*/ */
public function getDeputyUseridsAttribute(): 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 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 public function isDeputyOwner($userid): bool
{ {
@ -138,7 +138,7 @@ class Project extends AbstractModel
} }
/** /**
* 是否负责人(主或副 * 是否负责人(含项目管理员
*/ */
public function isOwner($userid): bool public function isOwner($userid): bool
{ {
@ -693,8 +693,8 @@ class Project extends AbstractModel
* 获取项目信息(用于判断会员是否存在项目内) * 获取项目信息(用于判断会员是否存在项目内)
* @param int $project_id * @param int $project_id
* @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制 * @param null|bool $archived true:仅限未归档, false:仅限已归档, null:不限制
* @param null|bool|string $mustOwner true:主或副都可(主+共享操作); * @param null|bool|string $mustOwner true:负责人或项目管理员都可(共享操作);
* 'primary':主(转让/删除/任命副等主独占操作); * 'primary':负责人(转让/删除/任命项目管理员等独占操作);
* false:仅限非负责人null:不限制 * false:仅限非负责人null:不限制
* @return self * @return self
*/ */

View File

@ -1993,7 +1993,7 @@ class ProjectTask extends AbstractModel
// //
$projectOwnerids = ProjectUser::whereProjectId($this->project_id) $projectOwnerids = ProjectUser::whereProjectId($this->project_id)
->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY]) ->whereIn('owner', [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY])
->pluck('userid')->toArray(); // 项目负责人(主+副 ->pluck('userid')->toArray(); // 项目负责人(含项目管理员
// //
$array = []; $array = [];
if (empty($userids)) { if (empty($userids)) {

View File

@ -39,13 +39,13 @@ class ProjectUser extends AbstractModel
{ {
/** @var int 普通成员编码 */ /** @var int 普通成员编码 */
const OWNER_MEMBER = 0; const OWNER_MEMBER = 0;
/** @var int 负责人编码 */ /** @var int 项目负责人编码 */
const OWNER_PRIMARY = 1; const OWNER_PRIMARY = 1;
/** @var int 副负责人编码 */ /** @var int 项目管理员编码 */
const OWNER_DEPUTY = 2; const OWNER_DEPUTY = 2;
/** /**
* 是否负责人owner=1 * 是否项目负责人owner=1
*/ */
public function isPrimaryOwner(): bool public function isPrimaryOwner(): bool
{ {
@ -53,7 +53,7 @@ class ProjectUser extends AbstractModel
} }
/** /**
* 是否副负责人owner=2 * 是否项目管理员owner=2
*/ */
public function isDeputyOwner(): bool public function isDeputyOwner(): bool
{ {
@ -61,7 +61,7 @@ class ProjectUser extends AbstractModel
} }
/** /**
* 是否负责人(主或副 * 是否负责人(含项目管理员
*/ */
public function isOwner(): bool public function isOwner(): bool
{ {
@ -91,8 +91,8 @@ class ProjectUser extends AbstractModel
foreach ($list as $item) { foreach ($list as $item) {
$row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first(); $row = self::whereProjectId($item->project_id)->whereUserid($newUserid)->first();
if ($row) { if ($row) {
// 已存在:仅当离职用户是owner=1时把接收人升为主 // 已存在:仅当离职用户是项目负责人owner=1时把接收人升为项目负责人
// 离职用户是owner=2时不传副给接收人spec不替补) // 离职用户是项目管理员owner=2时不传项目管理员身份给接收人spec项目管理员不替补)
if ((int)$item->owner === self::OWNER_PRIMARY) { if ((int)$item->owner === self::OWNER_PRIMARY) {
$row->owner = self::OWNER_PRIMARY; $row->owner = self::OWNER_PRIMARY;
} }
@ -100,7 +100,7 @@ class ProjectUser extends AbstractModel
$row->save(); $row->save();
$item->delete(); $item->delete();
} else { } else {
// 不存在:转移时如果离职用户是副,降级为普通成员(不带副身份过户给接收人) // 不存在:转移时如果离职用户是项目管理员,降级为普通成员(不带项目管理员身份过户给接收人)
if ((int)$item->owner === self::OWNER_DEPUTY) { if ((int)$item->owner === self::OWNER_DEPUTY) {
$item->owner = self::OWNER_MEMBER; $item->owner = self::OWNER_MEMBER;
} }

View File

@ -55,7 +55,7 @@ class UserDepartment extends AbstractModel
} }
/** /**
* 副负责人 userid 列表 * 部门管理员 userid 列表
* @return array * @return array
*/ */
public function getDeputyUseridsAttribute(): array public function getDeputyUseridsAttribute(): array
@ -71,7 +71,7 @@ class UserDepartment extends AbstractModel
} }
/** /**
* 是否负责人(与 owner_userid 一致) * 是否部门负责人(与 owner_userid 一致)
*/ */
public function isPrimaryOwner($userid): bool public function isPrimaryOwner($userid): bool
{ {
@ -82,7 +82,7 @@ class UserDepartment extends AbstractModel
} }
/** /**
* 是否副负责人(在 user_department_owners 表里) * 是否部门管理员(在 user_department_owners 表里)
*/ */
public function isDeputyOwner($userid): bool public function isDeputyOwner($userid): bool
{ {
@ -96,7 +96,7 @@ class UserDepartment extends AbstractModel
} }
/** /**
* 是否负责人(主或副 * 是否负责人(含部门管理员
*/ */
public function isOwner($userid): bool public function isOwner($userid): bool
{ {
@ -127,7 +127,7 @@ class UserDepartment extends AbstractModel
$dialog->owner_id = $this->owner_userid; $dialog->owner_id = $this->owner_userid;
if ($dialog->save()) { if ($dialog->save()) {
$dialog->joinGroup($this->owner_userid, 0, true); $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) { if ($oldOwnerId > 0 && $oldOwnerId !== (int)$this->owner_userid) {
WebSocketDialogUser::where('dialog_id', $dialog->id) WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $oldOwnerId) ->where('userid', $oldOwnerId)
@ -156,9 +156,9 @@ class UserDepartment extends AbstractModel
$dialog->group_type = 'department'; $dialog->group_type = 'department';
if ($dialog->save()) { if ($dialog->save()) {
$dialog->joinGroup($this->owner_userid, 0, true); $dialog->joinGroup($this->owner_userid, 0, true);
// 同步 role主 role=0、新主 role=1、原副 role=0 // 同步 role负责人 role=0、新负责人 role=1、原部门管理员 role=0
// 原清零:避免 dialog_users.role=2 与 user_department_owners 不一致 // 原部门管理员清零:避免 dialog_users.role=2 与 user_department_owners 不一致
// 关系不带过来,须通过 addDeputy 显式重新任命) // 部门管理员关系不带过来,须通过 addDeputy 显式重新任命)
if ($oldOwnerId > 0 && $oldOwnerId !== (int)$this->owner_userid) { if ($oldOwnerId > 0 && $oldOwnerId !== (int)$this->owner_userid) {
WebSocketDialogUser::where('dialog_id', $dialog->id) WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $oldOwnerId) ->where('userid', $oldOwnerId)
@ -214,10 +214,10 @@ class UserDepartment extends AbstractModel
} }
/** /**
* 任命副负责人 * 任命部门管理员
* - 副自动加入 users.department成为部门成员与主对齐) * - 部门管理员自动加入 users.department成为部门成员与负责人对齐)
* - 自动加入部门群 + role=2 * - 部门管理员自动加入部门群 + role=2
* - 幂等(已是不报错) * - 幂等(已是部门管理员不报错)
* *
* @param int $userid * @param int $userid
* @return void * @return void
@ -237,13 +237,13 @@ class UserDepartment extends AbstractModel
} }
AbstractModel::transaction(function () use ($userid, $user) { AbstractModel::transaction(function () use ($userid, $user) {
// 写unique key 自动幂等) // 写部门管理员unique key 自动幂等)
\DB::table('user_department_owners')->insertOrIgnore([ \DB::table('user_department_owners')->insertOrIgnore([
'department_id' => $this->id, 'department_id' => $this->id,
'userid' => $userid, 'userid' => $userid,
]); ]);
// 加入 users.department成为部门成员对齐) // 加入 users.department成为部门成员负责人对齐)
$userDeptIds = $user->department; // accessor 返回数组 $userDeptIds = $user->department; // accessor 返回数组
if (!in_array($this->id, $userDeptIds)) { if (!in_array($this->id, $userDeptIds)) {
$userDeptIds = array_merge($userDeptIds, [$this->id]); $userDeptIds = array_merge($userDeptIds, [$this->id]);
@ -251,7 +251,7 @@ class UserDepartment extends AbstractModel
$user->save(); $user->save();
} }
// 加入部门群 + 设 role=2 // 加部门管理员入部门群 + 设 role=2
if ($this->dialog_id > 0) { if ($this->dialog_id > 0) {
$dialog = WebSocketDialog::find($this->dialog_id); $dialog = WebSocketDialog::find($this->dialog_id);
if ($dialog) { 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 = WebSocketDialog::find($this->dialog_id);
$dialog?->deleteDialog(); $dialog?->deleteDialog();
// 清理副负责人记录(防悬挂) // 清理部门管理员记录(防悬挂)
\DB::table('user_department_owners')->where('department_id', $this->id)->delete(); \DB::table('user_department_owners')->where('department_id', $this->id)->delete();
// //
$this->delete(); $this->delete();
@ -355,7 +355,7 @@ class UserDepartment extends AbstractModel
*/ */
public static function transfer($originalUserid, $newUserid) public static function transfer($originalUserid, $newUserid)
{ {
// 转让(保持现有逻辑) // 部门负责人转让(保持现有逻辑)
self::whereOwnerUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) { self::whereOwnerUserid($originalUserid)->chunkById(100, function ($list) use ($originalUserid, $newUserid) {
/** @var self $item */ /** @var self $item */
foreach ($list as $item) { foreach ($list as $item) {
@ -364,7 +364,7 @@ class UserDepartment extends AbstractModel
]); ]);
} }
}); });
// 副离职清理(新增):直接删除离职用户的所有副记录 // 部门管理员离职清理(新增):直接删除离职用户的所有部门管理员记录
// 不需要清群 role —— UserTransfer::exitDialog 会把人踢出所有群role 随成员关系一起消失 // 不需要清群 role —— UserTransfer::exitDialog 会把人踢出所有群role 随成员关系一起消失
\DB::table('user_department_owners') \DB::table('user_department_owners')
->where('userid', $originalUserid) ->where('userid', $originalUserid)

View File

@ -520,7 +520,7 @@ class WebSocketDialog extends AbstractModel
foreach ($list as $item) { foreach ($list as $item) {
if ($checkDelete) { if ($checkDelete) {
if ($type === 'remove') { if ($type === 'remove') {
// 移出时:如果是全员群仅允许管理员操作,其他群主/副群主/邀请人可以操作 // 移出时:如果是全员群仅允许管理员操作,其他群主/群管理员/邀请人可以操作
if ($this->group_type === 'all') { if ($this->group_type === 'all') {
User::auth("admin"); User::auth("admin");
} else { } else {
@ -529,12 +529,12 @@ class WebSocketDialog extends AbstractModel
if ($actor <= 0) { if ($actor <= 0) {
throw new ApiException('只有群主或邀请人可以移出成员'); throw new ApiException('只有群主或邀请人可以移出成员');
} }
// 主群主、副群主、邀请人可移出 // 群主、群管理员、邀请人可移出
$allowedActor = $this->isOwner($actor) || $actor === (int)$item->inviter; $allowedActor = $this->isOwner($actor) || $actor === (int)$item->inviter;
if (!$allowedActor) { if (!$allowedActor) {
throw new ApiException('只有群主或邀请人可以移出成员'); throw new ApiException('只有群主或邀请人可以移出成员');
} }
// 副群主不能移出主群主或其他副群主 // 群管理员不能移出群主或其他群管理员
if ($this->isDeputyOwner($actor)) { if ($this->isDeputyOwner($actor)) {
$targetIsOwner = $this->isPrimaryOwner($item->userid) || $this->isDeputyOwner($item->userid); $targetIsOwner = $this->isPrimaryOwner($item->userid) || $this->isDeputyOwner($item->userid);
if ($targetIsOwner) { if ($targetIsOwner) {
@ -659,7 +659,7 @@ class WebSocketDialog extends AbstractModel
} }
/** /**
* 是否群主(与 owner_id 一致) * 是否群主(与 owner_id 一致)
*/ */
public function isPrimaryOwner($userid): bool 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 public function isDeputyOwner($userid): bool
{ {
@ -681,7 +681,7 @@ class WebSocketDialog extends AbstractModel
} }
/** /**
* 是否群主(主或副 * 是否群主(含群管理员
*/ */
public function isOwner($userid): bool public function isOwner($userid): bool
{ {
@ -689,7 +689,7 @@ class WebSocketDialog extends AbstractModel
} }
/** /**
* 副群主 userid 列表 * 群管理员 userid 列表
* *
* @return array * @return array
*/ */

View File

@ -16,7 +16,7 @@ class AddRoleToWebSocketDialogUsers extends Migration
Schema::table('web_socket_dialog_users', function (Blueprint $table) { Schema::table('web_socket_dialog_users', function (Blueprint $table) {
if (!Schema::hasColumn('web_socket_dialog_users', 'role')) { if (!Schema::hasColumn('web_socket_dialog_users', 'role')) {
$table->tinyInteger('role')->default(0)->after('userid') $table->tinyInteger('role')->default(0)->after('userid')
->comment('0=普通成员 1=主群主 2=副群主'); ->comment('0=普通成员 1=群主 2=群管理员');
$table->index(['dialog_id', 'role'], 'idx_dialog_role'); $table->index(['dialog_id', 'role'], 'idx_dialog_role');
} }
}); });

View File

@ -17,7 +17,7 @@ class CreateUserDepartmentOwnersTable extends Migration
Schema::create('user_department_owners', function (Blueprint $table) { Schema::create('user_department_owners', function (Blueprint $table) {
$table->bigIncrements('id'); $table->bigIncrements('id');
$table->unsignedBigInteger('department_id')->comment('部门ID'); $table->unsignedBigInteger('department_id')->comment('部门ID');
$table->unsignedBigInteger('userid')->comment('副负责人 userid'); $table->unsignedBigInteger('userid')->comment('部门管理员 userid');
$table->timestamp('created_at')->useCurrent(); $table->timestamp('created_at')->useCurrent();
$table->unique(['department_id', 'userid'], 'uniq_dept_user'); $table->unique(['department_id', 'userid'], 'uniq_dept_user');
$table->index('userid', 'idx_userid'); $table->index('userid', 'idx_userid');

View File

@ -774,7 +774,7 @@ export default {
}, },
deputyWaitDemote() { deputyWaitDemote() {
// 使 // 使
const {deputy_userids = [], deputy_useridbak = []} = this.userData; const {deputy_userids = [], deputy_useridbak = []} = this.userData;
return deputy_useridbak.filter(id => !deputy_userids.includes(id)); return deputy_useridbak.filter(id => !deputy_userids.includes(id));
}, },
@ -822,7 +822,7 @@ export default {
}, },
memberRowUncancelable() { memberRowUncancelable() {
// + // +
if (!this.projectData) return []; if (!this.projectData) return [];
const deputies = (this.userData && Array.isArray(this.userData.deputy_userids)) const deputies = (this.userData && Array.isArray(this.userData.deputy_userids))
? this.userData.deputy_userids ? this.userData.deputy_userids
@ -834,13 +834,13 @@ export default {
}, },
deputyRowUncancelable() { deputyRowUncancelable() {
// v-model // v-model
if (!this.projectData) return []; if (!this.projectData) return [];
return [this.projectData.owner_userid]; return [this.projectData.owner_userid];
}, },
deputyRowDisabledChoice() { deputyRowDisabledChoice() {
// //
if (!this.projectData) return []; if (!this.projectData) return [];
return [this.projectData.owner_userid]; return [this.projectData.owner_userid];
}, },
@ -1169,7 +1169,7 @@ export default {
this.handleColumnDebounce(100); this.handleColumnDebounce(100);
}, },
'userData.deputy_userids'(newDeputies) { 'userData.deputy_userids'(newDeputies) {
// //
if (!Array.isArray(newDeputies) || !Array.isArray(this.userData.userids)) { if (!Array.isArray(newDeputies) || !Array.isArray(this.userData.userids)) {
return; return;
} }
@ -1507,7 +1507,7 @@ export default {
onUser() { onUser() {
this.userLoad++; this.userLoad++;
// deputy userid // deputy userid
const baseUserids = (this.userData.userids || []).slice(); const baseUserids = (this.userData.userids || []).slice();
const deputyUserids = (this.userData.deputy_userids || []).slice(); const deputyUserids = (this.userData.deputy_userids || []).slice();
const mergedUserids = Array.from(new Set([...baseUserids, ...deputyUserids])); const mergedUserids = Array.from(new Set([...baseUserids, ...deputyUserids]));
@ -1516,7 +1516,7 @@ export default {
project_id: this.projectId, project_id: this.projectId,
userid: mergedUserids, userid: mergedUserids,
}; };
// deputy_userid/ // deputy_userid/
if (this.canManageDeputy) { if (this.canManageDeputy) {
payload.deputy_userid = deputyUserids; payload.deputy_userid = deputyUserids;
} }

View File

@ -833,7 +833,7 @@ export default {
}, },
deputyDisabledChoice() { deputyDisabledChoice() {
// //
return [ return [
...(this.departmentData.owner_userid || []), ...(this.departmentData.owner_userid || []),
]; ];
@ -1088,8 +1088,8 @@ export default {
}); });
$A.messageSuccess(res.msg); $A.messageSuccess(res.msg);
// / // /
// departmentList // departmentList
let targetId = this.departmentData.id; let targetId = this.departmentData.id;
let oldDeputies = []; let oldDeputies = [];
if (targetId > 0) { if (targetId > 0) {

View File

@ -122,7 +122,7 @@ class MultiOwnerDepartmentTest extends TestCase
$member = $this->makeUser('d3_m@test.local'); $member = $this->makeUser('d3_m@test.local');
$dept = $this->makeDepartment($owner->userid); $dept = $this->makeDepartment($owner->userid);
// 手动插入记录addDeputy 在 Task 5 才实现) // 手动插入部门管理员记录addDeputy 在 Task 5 才实现)
DB::table('user_department_owners')->insert([ DB::table('user_department_owners')->insert([
'department_id' => $dept->id, 'department_id' => $dept->id,
'userid' => $deputy->userid, 'userid' => $deputy->userid,
@ -184,7 +184,7 @@ class MultiOwnerDepartmentTest extends TestCase
$deputy = $this->makeUser('d4b_dep@test.local'); $deputy = $this->makeUser('d4b_dep@test.local');
$dept = $this->makeDepartment($oldOwner->userid); $dept = $this->makeDepartment($oldOwner->userid);
// 加 deputy 入群 + 记录 + role=2pushMsg=false 跳过 Swoole // 加 deputy 入群 + 部门管理员记录 + role=2pushMsg=false 跳过 Swoole
$dialog = WebSocketDialog::find($dept->dialog_id); $dialog = WebSocketDialog::find($dept->dialog_id);
$dialog->joinGroup($deputy->userid, 0, null, false); $dialog->joinGroup($deputy->userid, 0, null, false);
DB::table('user_department_owners')->insert([ DB::table('user_department_owners')->insert([
@ -204,9 +204,9 @@ class MultiOwnerDepartmentTest extends TestCase
'owner_userid' => $newOwner->userid, 'owner_userid' => $newOwner->userid,
]); ]);
// 表保留 // 部门管理员表保留
$this->assertContains($deputy->userid, $dept->fresh()->deputy_userids); $this->assertContains($deputy->userid, $dept->fresh()->deputy_userids);
// role 保留 // 部门管理员 role 保留
$depRole = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) $depRole = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
->where('userid', $deputy->userid)->value('role'); ->where('userid', $deputy->userid)->value('role');
$this->assertEquals(2, (int)$depRole); $this->assertEquals(2, (int)$depRole);
@ -222,11 +222,11 @@ class MultiOwnerDepartmentTest extends TestCase
$dept = $dept->fresh(); $dept = $dept->fresh();
$this->assertContains($deputy->userid, $dept->deputy_userids); $this->assertContains($deputy->userid, $dept->deputy_userids);
// 已入群 // 部门管理员已入群
$exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) $exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
->where('userid', $deputy->userid)->exists(); ->where('userid', $deputy->userid)->exists();
$this->assertTrue($exists); $this->assertTrue($exists);
// role=2 // 部门管理员 role=2
$role = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) $role = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
->where('userid', $deputy->userid)->value('role'); ->where('userid', $deputy->userid)->value('role');
$this->assertEquals(2, (int)$role); $this->assertEquals(2, (int)$role);
@ -272,23 +272,23 @@ class MultiOwnerDepartmentTest extends TestCase
$dept = $this->makeDepartment($owner->userid); $dept = $this->makeDepartment($owner->userid);
$this->simulateAddDeputy($dept, $deputy->userid); $this->simulateAddDeputy($dept, $deputy->userid);
// 任命后应该入 users.department 并加入部门群 // 任命后部门管理员应该入 users.department 并加入部门群
$this->assertContains($dept->id, User::find($deputy->userid)->department, '任命后应加入 users.department'); $this->assertContains($dept->id, User::find($deputy->userid)->department, '任命部门管理员后应加入 users.department');
$this->assertTrue( $this->assertTrue(
WebSocketDialogUser::where('dialog_id', $dept->dialog_id)->where('userid', $deputy->userid)->exists(), WebSocketDialogUser::where('dialog_id', $dept->dialog_id)->where('userid', $deputy->userid)->exists(),
'任命后应加入部门群' '任命部门管理员后应加入部门群'
); );
$this->simulateDelDeputy($dept, $deputy->userid); $this->simulateDelDeputy($dept, $deputy->userid);
$dept = $dept->fresh(); $dept = $dept->fresh();
$this->assertNotContains($deputy->userid, $dept->deputy_userids); $this->assertNotContains($deputy->userid, $dept->deputy_userids);
// 罢免后从 users.department 移除(与"离开部门"对齐) // 罢免后从 users.department 移除(与负责人"离开部门"对齐)
$this->assertNotContains($dept->id, User::find($deputy->userid)->department, '罢免后应从 users.department 移除'); $this->assertNotContains($dept->id, User::find($deputy->userid)->department, '罢免部门管理员后应从 users.department 移除');
// 退出部门群(成员关系=群关系一致) // 退出部门群(成员关系=群关系一致)
$exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id) $exists = WebSocketDialogUser::where('dialog_id', $dept->dialog_id)
->where('userid', $deputy->userid)->exists(); ->where('userid', $deputy->userid)->exists();
$this->assertFalse($exists, '罢免后应退出部门群(成员关系=群关系)'); $this->assertFalse($exists, '罢免部门管理员后应退出部门群(成员关系=群关系)');
} }
public function test_delDeputy_idempotent_for_non_deputy() public function test_delDeputy_idempotent_for_non_deputy()
@ -297,7 +297,7 @@ class MultiOwnerDepartmentTest extends TestCase
$member = $this->makeUser('d6b_m@test.local'); $member = $this->makeUser('d6b_m@test.local');
$dept = $this->makeDepartment($owner->userid); $dept = $this->makeDepartment($owner->userid);
// member 不是,调 delDeputy 不应抛错 // member 不是部门管理员,调 delDeputy 不应抛错
$this->simulateDelDeputy($dept, $member->userid); $this->simulateDelDeputy($dept, $member->userid);
$this->assertTrue(true); $this->assertTrue(true);
} }
@ -326,15 +326,15 @@ class MultiOwnerDepartmentTest extends TestCase
UserDepartment::transfer($departing->userid, $receiver->userid); UserDepartment::transfer($departing->userid, $receiver->userid);
// 离职的记录已删 // 离职的部门管理员记录已删
$this->assertNotContains($departing->userid, $dept->fresh()->deputy_userids); $this->assertNotContains($departing->userid, $dept->fresh()->deputy_userids);
// receiver 没有继承身份 // receiver 没有继承部门管理员身份
$this->assertNotContains($receiver->userid, $dept->fresh()->deputy_userids); $this->assertNotContains($receiver->userid, $dept->fresh()->deputy_userids);
} }
public function test_user_transfer_inherits_departing_primary() public function test_user_transfer_inherits_departing_primary()
{ {
// 主转让仍要把主权位传给接收人(保留现有行为) // 部门负责人转让仍要把负责人身份传给接收人(保留现有行为)
$departing = $this->makeUser('d7c_dep@test.local'); $departing = $this->makeUser('d7c_dep@test.local');
$receiver = $this->makeUser('d7c_rec@test.local'); $receiver = $this->makeUser('d7c_rec@test.local');
$dept = $this->makeDepartment($departing->userid); $dept = $this->makeDepartment($departing->userid);
@ -346,7 +346,7 @@ class MultiOwnerDepartmentTest extends TestCase
public function test_deleteDepartment_recursively_cleans_child_deputies() public function test_deleteDepartment_recursively_cleans_child_deputies()
{ {
// 父部门 + 子部门各有副,删父部门时副记录应级联清理 // 父部门 + 子部门各有部门管理员,删父部门时部门管理员记录应级联清理
$owner = $this->makeUser('d7d_o@test.local'); $owner = $this->makeUser('d7d_o@test.local');
$deputyParent = $this->makeUser('d7d_dp@test.local'); $deputyParent = $this->makeUser('d7d_dp@test.local');
$deputyChild = $this->makeUser('d7d_dc@test.local'); $deputyChild = $this->makeUser('d7d_dc@test.local');
@ -365,7 +365,7 @@ class MultiOwnerDepartmentTest extends TestCase
$parentId = $parent->id; $parentId = $parent->id;
$childId = $child->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', $parentId)->count());
$this->assertEquals(0, DB::table('user_department_owners')->where('department_id', $childId)->count()); $this->assertEquals(0, DB::table('user_department_owners')->where('department_id', $childId)->count());

View File

@ -48,7 +48,7 @@ class MultiOwnerGroupTest extends TestCase
$member = $this->makeUser('m2@test.local'); $member = $this->makeUser('m2@test.local');
$dialog = $this->makeGroup($owner->userid, [$deputy->userid, $member->userid]); $dialog = $this->makeGroup($owner->userid, [$deputy->userid, $member->userid]);
// 手工把 deputy 设为副群主 // 手工把 deputy 设为群管理员
WebSocketDialogUser::where('dialog_id', $dialog->id) WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $deputy->userid) ->where('userid', $deputy->userid)
->update(['role' => 2]); ->update(['role' => 2]);
@ -111,13 +111,13 @@ class MultiOwnerGroupTest extends TestCase
->where('userid', $deputy->userid) ->where('userid', $deputy->userid)
->update(['role' => 2]); ->update(['role' => 2]);
// 模拟副群主退群pushMsg=false 跳过 Swoole 推送,仅验证 DB 状态) // 模拟群管理员退群pushMsg=false 跳过 Swoole 推送,仅验证 DB 状态)
$dialog->exitGroup($deputy->userid, 'exit', false, false); $dialog->exitGroup($deputy->userid, 'exit', false, false);
$exists = WebSocketDialogUser::where('dialog_id', $dialog->id) $exists = WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $deputy->userid) ->where('userid', $deputy->userid)
->exists(); ->exists();
$this->assertFalse($exists, '退群后副群主记录应被删除'); $this->assertFalse($exists, '退群后群管理员记录应被删除');
$this->assertNotContains($deputy->userid, $dialog->fresh()->deputy_ids); $this->assertNotContains($deputy->userid, $dialog->fresh()->deputy_ids);
} }
@ -358,13 +358,13 @@ class MultiOwnerGroupTest extends TestCase
return ['allowed' => false, 'error' => '目标用户不在群内']; return ['allowed' => false, 'error' => '目标用户不在群内'];
} }
// 主群主、副群主、邀请人可移出 // 群主、群管理员、邀请人可移出
$allowedActor = $dialog->isOwner($actorId) || $actorId === (int)$item->inviter; $allowedActor = $dialog->isOwner($actorId) || $actorId === (int)$item->inviter;
if (!$allowedActor) { if (!$allowedActor) {
return ['allowed' => false, 'error' => '只有群主或邀请人可以移出成员']; return ['allowed' => false, 'error' => '只有群主或邀请人可以移出成员'];
} }
// 副群主不能移出主群主或其他副群主 // 群管理员不能移出群主或其他群管理员
if ($dialog->isDeputyOwner($actorId)) { if ($dialog->isDeputyOwner($actorId)) {
$targetIsOwner = $dialog->isPrimaryOwner($targetId) || $dialog->isDeputyOwner($targetId); $targetIsOwner = $dialog->isPrimaryOwner($targetId) || $dialog->isDeputyOwner($targetId);
if ($targetIsOwner) { if ($targetIsOwner) {
@ -389,9 +389,9 @@ class MultiOwnerGroupTest extends TestCase
WebSocketDialogUser::where('dialog_id', $dialog->id) WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $deputy->userid)->update(['role' => 2]); ->where('userid', $deputy->userid)->update(['role' => 2]);
// 验证权限逻辑:副群主可移出普通成员 // 验证权限逻辑:群管理员可移出普通成员
$result = $this->simulateRemovePermission($dialog, $deputy->userid, $member->userid); $result = $this->simulateRemovePermission($dialog, $deputy->userid, $member->userid);
$this->assertTrue($result['allowed'], '副群主应能移出普通成员,错误:' . ($result['error'] ?? '')); $this->assertTrue($result['allowed'], '群管理员应能移出普通成员,错误:' . ($result['error'] ?? ''));
// 验证实际移出操作checkDelete=false 绕过 auth直接测试 DB 状态) // 验证实际移出操作checkDelete=false 绕过 auth直接测试 DB 状态)
$dialog->exitGroup($member->userid, 'remove', false, false); $dialog->exitGroup($member->userid, 'remove', false, false);
@ -408,9 +408,9 @@ class MultiOwnerGroupTest extends TestCase
WebSocketDialogUser::where('dialog_id', $dialog->id) WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $deputy->userid)->update(['role' => 2]); ->where('userid', $deputy->userid)->update(['role' => 2]);
// 验证权限逻辑:副群主不可移出主群主 // 验证权限逻辑:群管理员不可移出群主
$result = $this->simulateRemovePermission($dialog, $deputy->userid, $owner->userid); $result = $this->simulateRemovePermission($dialog, $deputy->userid, $owner->userid);
$this->assertFalse($result['allowed'], '副群主不应能移出主群主'); $this->assertFalse($result['allowed'], '群管理员不应能移出群主');
$this->assertNotNull($result['error']); $this->assertNotNull($result['error']);
} }
@ -424,9 +424,9 @@ class MultiOwnerGroupTest extends TestCase
->whereIn('userid', [$deputy1->userid, $deputy2->userid]) ->whereIn('userid', [$deputy1->userid, $deputy2->userid])
->update(['role' => 2]); ->update(['role' => 2]);
// 验证权限逻辑:副群主不可移出其他副群主 // 验证权限逻辑:群管理员不可移出其他群管理员
$result = $this->simulateRemovePermission($dialog, $deputy1->userid, $deputy2->userid); $result = $this->simulateRemovePermission($dialog, $deputy1->userid, $deputy2->userid);
$this->assertFalse($result['allowed'], '副群主不应能移出其他副群主'); $this->assertFalse($result['allowed'], '群管理员不应能移出其他群管理员');
$this->assertEquals('群管理员不能移出群主或其他群管理员', $result['error']); $this->assertEquals('群管理员不能移出群主或其他群管理员', $result['error']);
} }
@ -466,11 +466,11 @@ class MultiOwnerGroupTest extends TestCase
} }
/** /**
* 验证离职移交时副群主角色被正确清除。 * 验证离职移交时群管理员角色被正确清除。
* *
* UserTransfer::exitDialog() 对离职用户调用 exitGroup($original_userid, 'remove', false, false) * UserTransfer::exitDialog() 对离职用户调用 exitGroup($original_userid, 'remove', false, false)
* exitGroup 内部直接 hard-delete web_socket_dialog_users 记录($item->delete() * exitGroup 内部直接 hard-delete web_socket_dialog_users 记录($item->delete()
* 因此副群主role 随记录一起消失,无需额外逻辑。 * 因此群管理员role 随记录一起消失,无需额外逻辑。
* *
* 本测试直接调用 exitDialog()(通过 UserTransfer 实例),绕过 start() 中的项目/任务/文件迁移, * 本测试直接调用 exitDialog()(通过 UserTransfer 实例),绕过 start() 中的项目/任务/文件迁移,
* 以确保在无 Swoole 推送的 PHPUnit 环境中可以正常运行。 * 以确保在无 Swoole 推送的 PHPUnit 环境中可以正常运行。
@ -482,13 +482,13 @@ class MultiOwnerGroupTest extends TestCase
$receiver = $this->makeUser('rec11@test.local'); $receiver = $this->makeUser('rec11@test.local');
$dialog = $this->makeGroup($owner->userid, [$departing->userid, $receiver->userid]); $dialog = $this->makeGroup($owner->userid, [$departing->userid, $receiver->userid]);
// 将离职用户设为副群主 // 将离职用户设为群管理员
WebSocketDialogUser::where('dialog_id', $dialog->id) WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $departing->userid) ->where('userid', $departing->userid)
->update(['role' => 2]); ->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 // 通过 UserTransfer 触发 exitDialog使用正确字段名 original_userid / new_userid
$transfer = \App\Models\UserTransfer::createInstance([ $transfer = \App\Models\UserTransfer::createInstance([
@ -500,14 +500,14 @@ class MultiOwnerGroupTest extends TestCase
$freshDialog = $dialog->fresh(); $freshDialog = $dialog->fresh();
// 离职用户不应再出现在副群主列表中 // 离职用户不应再出现在群管理员列表中
$this->assertNotContains($departing->userid, $freshDialog->deputy_ids, '离职用户不应留在副群主列表'); $this->assertNotContains($departing->userid, $freshDialog->deputy_ids, '离职用户不应留在群管理员列表');
// 离职用户的成员记录应已删除 // 离职用户的成员记录应已删除
$exists = WebSocketDialogUser::where('dialog_id', $dialog->id) $exists = WebSocketDialogUser::where('dialog_id', $dialog->id)
->where('userid', $departing->userid) ->where('userid', $departing->userid)
->exists(); ->exists();
$this->assertFalse($exists, 'exitDialog 后离职用户的成员记录应被删除'); $this->assertFalse($exists, 'exitDialog 后离职用户的成员记录应被删除');
// 接收方不应自动继承副群主角色 // 接收方不应自动继承群管理员角色
$this->assertNotContains($receiver->userid, $freshDialog->deputy_ids, '接收方不应自动继承副群主角色'); $this->assertNotContains($receiver->userid, $freshDialog->deputy_ids, '接收方不应自动继承群管理员角色');
} }
} }

View File

@ -117,17 +117,17 @@ class MultiOwnerProjectTest extends TestCase
} }
/** /**
* 模拟合并后的 ProjectController::user() 端点:同步成员 + 副负责人 * 模拟合并后的 ProjectController::user() 端点:同步成员 + 项目管理员
* *
* @param Project $project 项目实例 * @param Project $project 项目实例
* @param int $callerUserid 调用方 userid用于权限判断 * @param int $callerUserid 调用方 userid用于权限判断
* @param int[] $userids 最终成员完整列表(必须包含负责人) * @param int[] $userids 最终成员完整列表(必须包含项目负责人)
* @param int[]|null $deputyUserids 最终副负责人完整列表null 表示不设置(沿用既有副 * @param int[]|null $deputyUserids 最终项目管理员完整列表null 表示不设置(沿用既有项目管理员
* @return int[] 被移除的成员 userids * @return int[] 被移除的成员 userids
*/ */
private function simulateMemberSync(Project $project, int $callerUserid, array $userids, ?array $deputyUserids): array private function simulateMemberSync(Project $project, int $callerUserid, array $userids, ?array $deputyUserids): array
{ {
// 鉴权:调用方必须是主或副 // 鉴权:调用方必须是项目负责人或项目管理员
$callerRow = ProjectUser::where('project_id', $project->id) $callerRow = ProjectUser::where('project_id', $project->id)
->where('userid', $callerUserid)->first(); ->where('userid', $callerUserid)->first();
if (!$callerRow || !in_array((int)$callerRow->owner, [ProjectUser::OWNER_PRIMARY, ProjectUser::OWNER_DEPUTY], true)) { 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); \App\Models\ProjectUser::transfer($departing->userid, $receiver->userid);
// 离职的已不在 project_users // 离职的项目管理员已不在 project_users
$this->assertFalse( $this->assertFalse(
ProjectUser::where('project_id', $project->id) ProjectUser::where('project_id', $project->id)
->where('userid', $departing->userid)->exists() ->where('userid', $departing->userid)->exists()
); );
// receiver 没有继承身份 // receiver 没有继承项目管理员身份
$row = ProjectUser::where('project_id', $project->id)->where('userid', $receiver->userid)->first(); $row = ProjectUser::where('project_id', $project->id)->where('userid', $receiver->userid)->first();
$this->assertNotEquals(2, (int)$row->owner); $this->assertNotEquals(2, (int)$row->owner);
$this->assertNotContains($receiver->userid, $project->fresh()->deputy_userids); $this->assertNotContains($receiver->userid, $project->fresh()->deputy_userids);