From 7f1ed49cc87ae3373a877e245f9801ff7e5a729f Mon Sep 17 00:00:00 2001 From: gr Date: Fri, 28 Aug 2026 14:06:56 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app/api/validate/LoginAccountValidate.php | 6 +++--- server/app/common/enum/user/UserTerminalEnum.php | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/app/api/validate/LoginAccountValidate.php b/server/app/api/validate/LoginAccountValidate.php index 7cbde9839..ec7a5415c 100755 --- a/server/app/api/validate/LoginAccountValidate.php +++ b/server/app/api/validate/LoginAccountValidate.php @@ -35,8 +35,8 @@ class LoginAccountValidate extends BaseValidate protected $rule = [ 'terminal' => 'require|in:' . UserTerminalEnum::WECHAT_MMP . ',' . UserTerminalEnum::WECHAT_OA . ',' - . UserTerminalEnum::H5 . ',' . UserTerminalEnum::PC . ',' . UserTerminalEnum::IOS . - ',' . UserTerminalEnum::ANDROID, + . UserTerminalEnum::H5 . ',' . UserTerminalEnum::PC . ',' . UserTerminalEnum::IOS + . ',' . UserTerminalEnum::ANDROID . ',' . UserTerminalEnum::WECOM_RPA, 'scene' => 'require|in:' . LoginEnum::ACCOUNT_PASSWORD . ',' . LoginEnum::MOBILE_CAPTCHA . '|checkConfig', 'account' => 'require', ]; @@ -158,4 +158,4 @@ class LoginAccountValidate extends BaseValidate } return '验证码错误'; } -} \ No newline at end of file +} diff --git a/server/app/common/enum/user/UserTerminalEnum.php b/server/app/common/enum/user/UserTerminalEnum.php index d6327a027..d40b619c2 100755 --- a/server/app/common/enum/user/UserTerminalEnum.php +++ b/server/app/common/enum/user/UserTerminalEnum.php @@ -28,6 +28,7 @@ class UserTerminalEnum const PC = 4;//电脑PC const IOS = 5;//苹果app const ANDROID = 6;//安卓app + const WECOM_RPA = 7;//企业微信客服桌面端 const ALL_TERMINAL = [ @@ -37,6 +38,7 @@ class UserTerminalEnum self::PC, self::IOS, self::ANDROID, + self::WECOM_RPA, ]; /** @@ -55,10 +57,11 @@ class UserTerminalEnum self::PC => '电脑PC', self::IOS => '苹果APP', self::ANDROID => '安卓APP', + self::WECOM_RPA => '企业微信客服桌面端', ]; if(true === $from){ return $desc; } return $desc[$from] ?? ''; } -} \ No newline at end of file +} From 5e22d423d4add1ac5e0bfc8b67e7eba4843a1fea Mon Sep 17 00:00:00 2001 From: gr Date: Fri, 28 Aug 2026 14:07:30 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/controller/DesktopController.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 server/app/api/controller/DesktopController.php diff --git a/server/app/api/controller/DesktopController.php b/server/app/api/controller/DesktopController.php new file mode 100644 index 000000000..fc7ca37ad --- /dev/null +++ b/server/app/api/controller/DesktopController.php @@ -0,0 +1,44 @@ +userInfo['terminal'] ?? 0) !== UserTerminalEnum::WECOM_RPA) { + return $this->fail('当前登录令牌不属于企业微信客服桌面端'); + } + + $user = User::where(['id' => $this->userId]) + ->field('id,sn,nickname,mobile,avatar,is_disable') + ->findOrEmpty(); + if ($user->isEmpty() || (int)$user['is_disable'] === YesNoEnum::YES) { + return $this->fail('用户不存在或已禁用'); + } + + return $this->data([ + 'user_id' => (int)$user['id'], + 'sn' => (string)$user['sn'], + 'nickname' => (string)$user['nickname'], + 'mobile' => (string)$user['mobile'], + 'avatar' => (string)$user['avatar'], + 'terminal' => UserTerminalEnum::WECOM_RPA, + 'expire_time' => (int)($this->userInfo['expire_time'] ?? 0), + 'status' => 'active', + ]); + } +} From 1b8e166b33b9c8a1daeccc008c0af5c2e46b0801 Mon Sep 17 00:00:00 2001 From: gr Date: Wed, 9 Sep 2026 09:58:12 +0800 Subject: [PATCH 3/4] =?UTF-8?q?i=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adminapi/controller/DesktopController.php | 55 +++++++++++++++++++ .../app/adminapi/validate/LoginValidate.php | 5 +- server/app/common/enum/AdminTerminalEnum.php | 4 +- server/tests/AdminDesktopAuthContractTest.php | 33 +++++++++++ 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 server/app/adminapi/controller/DesktopController.php create mode 100644 server/tests/AdminDesktopAuthContractTest.php diff --git a/server/app/adminapi/controller/DesktopController.php b/server/app/adminapi/controller/DesktopController.php new file mode 100644 index 000000000..7f06d9d00 --- /dev/null +++ b/server/app/adminapi/controller/DesktopController.php @@ -0,0 +1,55 @@ +request->adminInfo ?? []); + if (empty($identity) || empty($identity['admin_id'])) { + return $this->fail('登录已失效,请重新登录'); + } + if ((int)($identity['terminal'] ?? 0) !== AdminTerminalEnum::WECOM_RPA) { + return $this->fail('当前登录令牌不属于企业微信客服桌面端'); + } + + $admin = Admin::where(['id' => (int)$identity['admin_id']]) + ->field('id,account,name,avatar,disable') + ->findOrEmpty(); + if ($admin->isEmpty() || (int)$admin['disable'] === YesNoEnum::YES) { + return $this->fail('管理员不存在或已禁用'); + } + + $account = trim((string)$admin['account']); + $name = trim((string)$admin['name']); + return $this->data([ + // 加类型前缀,避免管理员 id 与普通用户 id 相同时落到同一个客服租户。 + 'user_id' => 'admin:' . (int)$admin['id'], + 'sn' => $account, + 'nickname' => $name !== '' ? $name : $account, + 'mobile' => '', + 'avatar' => (string)$admin['avatar'], + 'terminal' => AdminTerminalEnum::WECOM_RPA, + 'expire_time' => (int)($identity['expire_time'] ?? 0), + 'status' => 'active', + 'account_type' => 'admin', + ]); + } +} diff --git a/server/app/adminapi/validate/LoginValidate.php b/server/app/adminapi/validate/LoginValidate.php index 0cf405f77..a1d18d624 100755 --- a/server/app/adminapi/validate/LoginValidate.php +++ b/server/app/adminapi/validate/LoginValidate.php @@ -32,7 +32,8 @@ use think\facade\Log; class LoginValidate extends BaseValidate { protected $rule = [ - 'terminal' => 'require|in:' . AdminTerminalEnum::PC . ',' . AdminTerminalEnum::MOBILE, + 'terminal' => 'require|in:' . AdminTerminalEnum::PC . ',' . AdminTerminalEnum::MOBILE + . ',' . AdminTerminalEnum::WECOM_RPA, 'account' => 'require', 'password' => 'require|password', ]; @@ -143,4 +144,4 @@ class LoginValidate extends BaseValidate return in_array($flag, ['1', 'true', 'yes', 'on'], true); } -} \ No newline at end of file +} diff --git a/server/app/common/enum/AdminTerminalEnum.php b/server/app/common/enum/AdminTerminalEnum.php index e3dac5da5..63585a9dd 100755 --- a/server/app/common/enum/AdminTerminalEnum.php +++ b/server/app/common/enum/AdminTerminalEnum.php @@ -25,4 +25,6 @@ class AdminTerminalEnum { const PC = 1; const MOBILE = 2; -} \ No newline at end of file + // 企业微信客服桌面软件。与网页端分开保存会话,桌面登录不会顶掉浏览器登录。 + const WECOM_RPA = 7; +} diff --git a/server/tests/AdminDesktopAuthContractTest.php b/server/tests/AdminDesktopAuthContractTest.php new file mode 100644 index 000000000..3b16d8641 --- /dev/null +++ b/server/tests/AdminDesktopAuthContractTest.php @@ -0,0 +1,33 @@ + 'admin:'") + && str_contains($controller, "'account_type' => 'admin'"), + 'desktop session verifies an admin token and namespaces its tenant identity' +); + +echo "OK\n"; From c1f728733131a37b5b2e58613ba574421faa58bd Mon Sep 17 00:00:00 2001 From: gr Date: Wed, 9 Sep 2026 10:48:05 +0800 Subject: [PATCH 4/4] igengx --- .../adminapi/controller/DesktopController.php | 18 ++- .../tests/AdminDesktopSessionBehaviorTest.php | 104 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 server/tests/AdminDesktopSessionBehaviorTest.php diff --git a/server/app/adminapi/controller/DesktopController.php b/server/app/adminapi/controller/DesktopController.php index 7f06d9d00..3c2b84333 100644 --- a/server/app/adminapi/controller/DesktopController.php +++ b/server/app/adminapi/controller/DesktopController.php @@ -5,9 +5,11 @@ namespace app\adminapi\controller; +use app\adminapi\logic\auth\AuthLogic; use app\common\enum\AdminTerminalEnum; use app\common\enum\YesNoEnum; use app\common\model\auth\Admin; +use app\common\model\auth\AdminRole; /** * 用最小身份信息把 ZYT 管理员账号映射到客服软件租户。 @@ -31,12 +33,23 @@ class DesktopController extends BaseAdminController } $admin = Admin::where(['id' => (int)$identity['admin_id']]) - ->field('id,account,name,avatar,disable') + ->field('id,account,name,avatar,disable,root') ->findOrEmpty(); if ($admin->isEmpty() || (int)$admin['disable'] === YesNoEnum::YES) { return $this->fail('管理员不存在或已禁用'); } + // 身份和权限在专用接口一次返回,客服后台无需跨 IP 调用网页端 mySelf。 + $roleIds = array_values(array_unique(array_map('intval', + AdminRole::where('admin_id', (int)$admin['id'])->column('role_id') + ))); + sort($roleIds); + $permissions = AuthLogic::getBtnAuthByRoleId([ + 'id' => (int)$admin['id'], + 'root' => (int)$admin['root'], + 'role_id' => $roleIds, + ]); + $account = trim((string)$admin['account']); $name = trim((string)$admin['name']); return $this->data([ @@ -50,6 +63,9 @@ class DesktopController extends BaseAdminController 'expire_time' => (int)($identity['expire_time'] ?? 0), 'status' => 'active', 'account_type' => 'admin', + 'root' => (int)$admin['root'] === YesNoEnum::YES, + 'role_ids' => $roleIds, + 'permissions' => array_values($permissions), ]); } } diff --git a/server/tests/AdminDesktopSessionBehaviorTest.php b/server/tests/AdminDesktopSessionBehaviorTest.php new file mode 100644 index 000000000..9fe878c34 --- /dev/null +++ b/server/tests/AdminDesktopSessionBehaviorTest.php @@ -0,0 +1,104 @@ + 9]) { throw new \RuntimeException('wrong identity lookup'); } + return new self(self::$row); + } + public function field(string $fields): self { return $this; } + public function findOrEmpty(): self { return $this; } + public function isEmpty(): bool { return $this->count() === 0; } + } + class AdminRole + { + public static function where(string $field, int $id): self + { + if ($field !== 'admin_id' || $id !== 9) { throw new \RuntimeException('wrong role lookup'); } + return new self(); + } + public function column(string $field): array { return ['3', '2', '3']; } + } +} +namespace app\adminapi\logic\auth { + class AuthLogic + { + public static array $permissions = ['tcm.diagnosis/lists']; + public static function getBtnAuthByRoleId(array $admin): array + { + if ($admin['id'] !== 9 || $admin['role_id'] !== [2, 3]) { + throw new \RuntimeException('wrong permission identity'); + } + return $admin['root'] ? ['*'] : self::$permissions; + } + } +} +namespace { + require dirname(__DIR__) . '/vendor/autoload.php'; + require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php'; + use app\adminapi\controller\DesktopController; + use app\adminapi\http\middleware\AuthMiddleware; + use app\adminapi\logic\auth\AuthLogic; + use app\common\model\auth\Admin; + + $app = new think\App(); + final class DesktopSessionFixture extends DesktopController + { + public function __construct(think\Request $request) { $this->request = $request; } + protected function data($data) { return ['code' => 1, 'data' => $data]; } + protected function fail(string $msg = 'fail', array $data = [], int $code = 0, int $show = 1) + { + return ['code' => $code, 'msg' => $msg]; + } + } + function desktopCheck(bool $ok, string $message): void + { + if (!$ok) { throw new RuntimeException($message); } + } + $valid = ['admin_id' => 9, 'terminal' => 7, 'expire_time' => time() + 3600, + 'root' => 0, 'login_ip' => '198.51.100.10']; + $invoke = function (array $identity, string $action = 'session') use ($app) { + $request = (new think\Request())->withServer(['REMOTE_ADDR' => '203.0.113.20']); + $request->setAction($action); + $request->setController('Desktop'); + $request->adminInfo = $identity; + $controller = new DesktopSessionFixture($request); + $request->controllerObject = $controller; + $app->instance('request', $request); + return (new AuthMiddleware())->handle($request, static fn() => $controller->session()); + }; + Admin::$row = ['id' => 9, 'account' => 'doctor9', 'name' => '医助', + 'avatar' => '', 'disable' => 0, 'root' => 0]; + $result = $invoke($valid); + desktopCheck($result['code'] === 1, 'valid desktop session works across different IPs'); + desktopCheck($result['data']['user_id'] === 'admin:9', 'account is namespaced'); + desktopCheck($result['data']['role_ids'] === [2, 3], 'current roles are normalized'); + desktopCheck($result['data']['permissions'] === ['tcm.diagnosis/lists'], 'current permissions returned'); + desktopCheck($result['data']['root'] === false, 'ordinary account stays ordinary'); + desktopCheck(!isset($result['data']['token']) && !isset($result['data']['login_ip']), 'minimal response'); + + AuthLogic::$permissions = []; + desktopCheck($invoke($valid)['data']['permissions'] === [], 'empty permissions remain empty'); + Admin::$row['root'] = 1; + desktopCheck($invoke($valid)['data']['permissions'] === ['*'], 'current root privilege returned'); + desktopCheck($invoke($valid)['data']['root'] === true, 'root comes from current account'); + Admin::$row['root'] = 0; + + desktopCheck($invoke([])['code'] === 0, 'missing or expired cached identity rejected'); + desktopCheck($invoke(array_replace($valid, ['terminal' => 1]))['code'] === 0, 'web token rejected'); + Admin::$row['disable'] = 1; + desktopCheck($invoke($valid)['code'] === 0, 'disabled account rejected'); + Admin::$row = []; + desktopCheck($invoke($valid)['code'] === 0, 'deleted account rejected'); + + $webResult = $invoke($valid, 'mySelf')->getData(); + desktopCheck($webResult['code'] === -1 && str_contains($webResult['msg'], 'ip地址'), + 'ordinary web requests retain IP validation'); + echo "AdminDesktopSessionBehaviorTest passed\n"; +} \ No newline at end of file