From a6284299343a01b62f19751a3eeedb821ac7d4d6 Mon Sep 17 00:00:00 2001 From: vilson <545522390@qq.com> Date: Mon, 24 Jun 2019 19:10:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E9=92=89=E9=92=89=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=92=89=E9=92=89=E6=89=AB=E7=A0=81=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: vilson <545522390@qq.com> --- application/common.php | 56 +++++++++++++- application/common/Model/Member.php | 61 ++++++++++++++-- application/common/Model/Organization.php | 2 +- application/index/controller/Oauth.php | 85 ++++++++++++++++++++++ application/project/common.php | 30 -------- application/project/controller/Index.php | 7 +- application/project/controller/Login.php | 50 ++++--------- composer.json | 3 +- config/dingtalk.php | 78 ++++++++++++++++++++ static/image/default/logo.png | Bin 0 -> 6160 bytes 10 files changed, 293 insertions(+), 79 deletions(-) create mode 100644 application/index/controller/Oauth.php create mode 100644 config/dingtalk.php create mode 100644 static/image/default/logo.png diff --git a/application/common.php b/application/common.php index 2e00677..b74b3fe 100644 --- a/application/common.php +++ b/application/common.php @@ -5,7 +5,7 @@ use service\NodeService; use service\RandomService; use think\Db; use think\facade\Cache; -use think\facade\Request; +use think\facade\Log; function isDebug() @@ -13,6 +13,54 @@ function isDebug() return config('app.app_debug'); } +/** + * 日志记录 + * @param string|array $content 内容 + * @param string $type 日志类型 + * @param string $path 日志地址 + */ +function logRecord($content, $type = 'info', $path = 'default') +{ + $path = 'log/' . $path; + Log::init(['path' => $path]); + if (is_array($content) || is_object($content)) { + $content = json_encode($content); + } + Log::write($content, $type); + Log::init(); +} + + +function getCurrentMember() +{ + return session('member'); +} + +function setCurrentMember($data) +{ + return session('member', $data); +} + +function getCurrentOrganizationCode() +{ + return session('currentOrganizationCode'); +} + +function setCurrentOrganizationCode($data) +{ + return session('currentOrganizationCode', $data); +} + +function getCurrentOrganization() +{ + return session('organization'); +} + +function setCurrentOrganization($data) +{ + return session('organization', $data); +} + /** * 打印输出数据到文件 * @param mixed $data 输出的数据 @@ -180,8 +228,8 @@ function decode($string) /** * 获取锁 - * @param String $key 锁标识 - * @param Int $expire 锁过期时间 + * @param String $key 锁标识 + * @param Int $expire 锁过期时间 * @return Boolean */ function lock($key = '', $expire = 5) @@ -197,7 +245,7 @@ function lock($key = '', $expire = 5) /** * 释放锁 - * @param String $key 锁标识 + * @param String $key 锁标识 * @return Boolean */ function unlock($key = '') diff --git a/application/common/Model/Member.php b/application/common/Model/Member.php index c64a313..e40f1f5 100644 --- a/application/common/Model/Member.php +++ b/application/common/Model/Member.php @@ -2,6 +2,9 @@ namespace app\common\Model; +use service\JwtService; +use service\NodeService; +use service\RandomService; use think\Db; use think\File; @@ -10,13 +13,38 @@ class Member extends CommonModel protected $append = []; - public function login($account) + public static function login($member) { - if ($account == 'admin') { - return []; + // 更新登录信息 + Db::name('Member')->where(['id' => $member['id']])->update([ + 'last_login_time' => Db::raw('now()'), + ]); + $list = MemberAccount::where(['member_code' => $member['code']])->order('id asc')->select()->toArray(); + $organizationList = []; + if ($list) { + foreach ($list as $item) { + $organization = Organization::where(['code' => $item['organization_code']])->find(); + if ($organization) { + $organizationList[] = $organization; + } + } } - $where[] = ['account', '=', $account]; - return Db::name('member')->where($where)->find(); + $member['account_id'] = $list[0]['id']; + $member['is_owner'] = $list[0]['is_owner']; + $member['authorize'] = $list[0]['authorize']; + $member['position'] = $list[0]['position']; + $member['department'] = $list[0]['department']; + + setCurrentMember($member); + !empty($member['authorize']) && NodeService::applyProjectAuthNode(); + $member = getCurrentMember(); + $tokenList = JwtService::initToken($member); + $accessTokenExp = JwtService::decodeToken($tokenList['accessToken'])->exp; + $tokenList['accessTokenExp'] = $accessTokenExp; + $loginInfo = ['member' => $member, 'tokenList' => $tokenList, 'organizationList' => $organizationList]; + session('loginInfo', $loginInfo); + logRecord($loginInfo, 'info', 'member/login'); + return $loginInfo; } /** @@ -30,6 +58,10 @@ class Member extends CommonModel { //需要创建的信息。1、用户 2、用户所属组织 3、组织权限 4、所属组织账号 $memberData['create_time'] = nowTime(); + (!isset($memberData['avatar']) || !$memberData['avatar']) && $memberData['avatar'] = 'https://static.vilson.xyz/cover.png'; + !isset($memberData['status']) && $memberData['status'] = 1; + !isset($memberData['code']) && $memberData['code'] = createUniqueCode('member'); + !isset($memberData['account']) && $memberData['account'] = RandomService::alnumLowercase(); $result = self::create($memberData); @@ -80,6 +112,25 @@ class Member extends CommonModel return $result; } + public static function dingtalkLogin($userInfo) + { + $unionid = $userInfo['unionid']; + $openid = $userInfo['openid']; + $member = self::where(['dingtalk_unionid' => $unionid])->find(); + if (!$member) { + $memberData = [ + 'dingtalk_openid' => $openid, + 'dingtalk_unionid' => $unionid, + 'name' => $userInfo['nick'], + 'avatar' => isset($userInfo['avatar']) ? $userInfo['avatar'] : '', + 'mobile' => isset($userInfo['mobile']) ? $userInfo['mobile'] : '', + 'email' => isset($userInfo['email']) ? $userInfo['email'] : '', + ]; + $member = self::createMember($memberData); + } + self::login($member); + return $member; + } /** * @param File $file diff --git a/application/common/Model/Organization.php b/application/common/Model/Organization.php index 1d3f1f0..a8f054e 100644 --- a/application/common/Model/Organization.php +++ b/application/common/Model/Organization.php @@ -65,7 +65,7 @@ class Organization extends CommonModel 'create_time' => nowTime(), 'avatar' => $memberData['avatar'], 'name' => $memberData['name'], - 'email' => $memberData['email'], + 'email' => isset($memberData['email']) ? $memberData['email'] : '', ]; MemberAccount::create($memberAccountData); return $organization; diff --git a/application/index/controller/Oauth.php b/application/index/controller/Oauth.php new file mode 100644 index 0000000..5e5beda --- /dev/null +++ b/application/index/controller/Oauth.php @@ -0,0 +1,85 @@ +user->getUseridByUnionid('3CnKFHEE7mX1hayPIHvpCwiEiE'); +// echo json_encode($userId);die; + $userId = $userId['userid']; + $user = $app->user->get($userId, $lang = null); + echo json_encode($user);die; + } + + public function dingTalkOauth() + { + $currentMember = getCurrentMember(); + if (!$currentMember) { + $app = new Application(config('dingtalk.')); + $response = $app->oauth->use('app-01')->withQrConnect()->redirect(); + $redirect = $response->getTargetUrl(); + if ($redirect) { + if ($redirect == 'undefined') { + $redirect = '/'; + } + $_SESSION['target_url'] = $redirect; + } + return redirect($redirect); + } else { + //已登录跳转 + return redirect(Request::domain() . '/index.html'); + } + } + + + /** + * 获取钉钉授权 + * @return RedirectResponse|Redirect + */ + public function dingTalkOauthCallback() + { + $app = new Application(config('dingtalk.')); + $data = Request::param(); + logRecord($data); + $user = $app->oauth->use('app-01')->user(); + $response = $app->oauth->use('app-01')->redirect(); + $redirect = $response->getTargetUrl(); + logRecord($redirect); + logRecord($user); + //用户注册/绑定微信 + $targetUrl = '/index.html#/member/login?logged=1'; + if (!$user['errcode']) { + $result = $app->user->getUseridByUnionid($user['user_info']['unionid']); + if (!$result['errcode']) { + $user['user_info']['avatar'] = $result['avatar']; + $user['user_info']['mobile'] = $result['mobile']; + $user['user_info']['email'] = $result['email']; + } + Member::dingtalkLogin($user['user_info']); + }else{ + $targetUrl = '/index.html#/member/login?logged=0&message=钉钉登录失败,请重试'; + } + // 登录成功跳转 + return redirect(Request::domain() . $targetUrl); + } +} diff --git a/application/project/common.php b/application/project/common.php index f8e4f72..7558499 100644 --- a/application/project/common.php +++ b/application/project/common.php @@ -37,33 +37,3 @@ function _uploadFile(File $file, $path_name = '', $saveName = false) -function getCurrentMember() -{ - return session('member'); -} - -function setCurrentMember($data) -{ - return session('member', $data); -} - -function getCurrentOrganizationCode() -{ - return session('currentOrganizationCode'); -} - -function setCurrentOrganizationCode($data) -{ - return session('currentOrganizationCode', $data); -} - -function getCurrentOrganization() -{ - return session('organization'); -} - -function setCurrentOrganization($data) -{ - return session('organization', $data); -} - diff --git a/application/project/controller/Index.php b/application/project/controller/Index.php index 22c8911..6b08d98 100644 --- a/application/project/controller/Index.php +++ b/application/project/controller/Index.php @@ -31,13 +31,13 @@ class Index extends BasicApi /** * @title 用户菜单 * @description 获取用户菜单列表 - * @author PearProject - * @url /project/index - * @method GET * @return void :名称 * @throws DataNotFoundException * @throws ModelNotFoundException * @throws DbException + * @author PearProject + * @url /project/index + * @method GET */ public function index() { @@ -81,7 +81,6 @@ class Index extends BasicApi } - /** * 个人个信息 * @throws DataNotFoundException diff --git a/application/project/controller/Login.php b/application/project/controller/Login.php index 18db453..63bbcd0 100644 --- a/application/project/controller/Login.php +++ b/application/project/controller/Login.php @@ -42,15 +42,15 @@ class Login extends BasicApi /** * @title 用户登录 * @description 用户登录 - * @author PearProject - * @url /project/login - * @method POST * @return void :名称 * @throws DataNotFoundException * @throws DbException * @throws ModelNotFoundException * @throws \think\Exception * @throws PDOException + * @author PearProject + * @url /project/login + * @method POST */ public function index() { @@ -92,34 +92,8 @@ class Login extends BasicApi if (!$mobile) { $member['password'] !== $data['password'] && $this->error('账号或密码错误', 201); } - // 更新登录信息 - Db::name('Member')->where(['id' => $member['id']])->update([ - 'last_login_time' => Db::raw('now()'), - ]); - $list = MemberAccount::where(['member_code' => $member['code']])->order('id asc')->select()->toArray(); - $organizationList = []; - if ($list) { - foreach ($list as $item) { - $organization = Organization::where(['code' => $item['organization_code']])->find(); - if ($organization) { - $organizationList[] = $organization; - } - } - } - $member['account_id'] = $list[0]['id']; - $member['is_owner'] = $list[0]['is_owner']; - $member['authorize'] = $list[0]['authorize']; - $member['position'] = $list[0]['position']; - $member['department'] = $list[0]['department']; - - setCurrentMember($member); - !empty($member['authorize']) && NodeService::applyProjectAuthNode(); - $member = getCurrentMember(); - Log::write(json_encode($member), "member-login"); - $tokenList = JwtService::initToken($member); - $accessTokenExp = JwtService::decodeToken($tokenList['accessToken'])->exp; - $tokenList['accessTokenExp'] = $accessTokenExp; - $this->success('', ['member' => $member, 'tokenList' => $tokenList, 'organizationList' => $organizationList]); + $result = Member::login($member); + $this->success('', $result); } /** @@ -307,15 +281,23 @@ class Login extends BasicApi } + public function _checkLogin() + { + $loginInfo = session('loginInfo'); + $this->success('', $loginInfo); + } + + /** * 退出登录 */ - public function out() + public function _out() { - session('user') && LogService::write('系统管理', '用户退出系统成功'); + session('member') && LogService::write('系统管理', '用户退出系统成功'); !empty($_SESSION) && $_SESSION = []; [session_unset(), session_destroy()]; - $this->success('退出登录成功!'); + session('loginInfo', null); + $this->success(''); } } diff --git a/composer.json b/composer.json index 4f9000c..e4768d0 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "overtrue/easy-sms": "^1.1", "phpoffice/phpspreadsheet": "^1.5", "firebase/php-jwt": "^5.0", - "phpmailer/phpmailer": "^6.0" + "phpmailer/phpmailer": "^6.0", + "mingyoung/dingtalk": "2.0" }, "autoload": { "psr-4": { diff --git a/config/dingtalk.php b/config/dingtalk.php new file mode 100644 index 0000000..3547dba --- /dev/null +++ b/config/dingtalk.php @@ -0,0 +1,78 @@ + 'ccc', + + /* + |----------------------------------------------------------- + | 【必填】应用 AppKey + |----------------------------------------------------------- + */ + 'app_key' => 'bbb', + + /* + |----------------------------------------------------------- + | 【必填】应用 AppSecret + |----------------------------------------------------------- + */ + 'app_secret' => 'aaa', + + /* + |----------------------------------------------------------- + | 【选填】加解密 + |----------------------------------------------------------- + | 此处的 `token` 和 `aes_key` 用于事件通知的加解密 + | 如果你用到事件回调功能,需要配置该两项 + */ + 'token' => 'uhl3CZbtsmf93bFPanmMenhWwrqbSwPc', + 'aes_key' => 'qZEOmHU2qYYk6n6vqLfi3FAhcp9bGA2kgbfnsXDrGgN', + + /* + |----------------------------------------------------------- + | 【选填】后台免登配置信息 + |----------------------------------------------------------- + | 如果你用到应用管理后台免登功能,需要配置该项 + */ + 'sso_secret' => 'Fx9_i5dSW5tpGtjalksdf98JF8uj32xb4NJQR5G9-VSchasd98asfdMmLR', + + /* + |----------------------------------------------------------- + | 【选填】第三方网站 OAuth 授权 + |----------------------------------------------------------- + | 如果你用到扫码登录、钉钉内免登和密码登录第三方网站,需要配置该项 + */ + 'oauth' => [ + /* + |------------------------------------------- + | `app-01` 为你自定义的名称,不要重复即可 + |------------------------------------------- + | 数组内需要配置 `client_id`, `client_secret`, `scope` 和 `redirect` 四项 + | + | `client_id` 为钉钉登录应用的 `appId` + | `client_secret` 为钉钉登录应用的 `appSecret` + | `scope`: + | - 扫码登录第三方网站和密码登录第三方网站填写 `snsapi_login` + | - 钉钉内免登第三方网站填写 `snsapi_auth` + | `redirect` 为回调地址 + */ + 'app-01' => [ + 'client_id' => 'aaa', + 'client_secret' => 'bbb-Tu2CsUZN-ZlQXWQ', + 'scope' => 'snsapi_login', + 'redirect' => 'https://example.com', + ], + /* + |------------------------------------------- + | 可配置多个 OAuth 应用,数组内内容同上 + |------------------------------------------- + */ + 'app-02' => [ + // ... + ] + ] +]; diff --git a/static/image/default/logo.png b/static/image/default/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f11f81eed7f6eb90ef206077982380168840ba9e GIT binary patch literal 6160 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