| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- <?php
- // +----------------------------------------------------------------------
- // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
- // +----------------------------------------------------------------------
- // | Copyright (c) 2017~2025 https://www.yiovo.com All rights reserved.
- // +----------------------------------------------------------------------
- // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
- // +----------------------------------------------------------------------
- // | Author: 萤火科技 <admin@yiovo.com>
- // +----------------------------------------------------------------------
- declare (strict_types=1);
- namespace app\api\service\passport;
- use app\api\model\{Setting as SettingModel, UploadFile as UploadFileModel, User as UserModel};
- use app\api\service\{passport\Party as PartyService, user\Oauth as OauthService};
- use app\api\validate\passport\Login as ValidateLogin;
- use app\common\enum\Client as ClientEnum;
- use app\common\enum\Setting as SettingEnum;
- use app\common\service\BaseService;
- use cores\exception\BaseException;
- use think\facade\Cache;
- /**
- * 服务类:用户登录
- * Class Login
- * @package app\api\service\passport
- */
- class Login extends BaseService
- {
- /**
- * 用户信息 (登录成功后才记录)
- * @var UserModel|null $userInfo
- */
- private ?UserModel $userInfo;
- // 用于生成token的自定义盐
- const TOKEN_SALT = 'user_salt';
- // 同一手机号允许的连续登录失败次数
- const LOGIN_FAIL_LIMIT = 5;
- // 登录失败锁定时间 (秒)
- const LOGIN_FAIL_LOCK_TIME = 600;
- /**
- * 执行用户登录 (手机号 + 密码)
- * @param array $data
- * @return bool
- * @throws BaseException
- * @throws \think\Exception
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- */
- public function login(array $data): bool
- {
- // 数据验证
- $this->validate($data);
- // 兜底: 第三方用户信息 (非必填)
- $data = \array_merge(['isParty' => false, 'partyData' => []], $data);
- // 手机号 + 密码登录 (未注册的手机号自动注册)
- $this->loginByPassword($data['mobile'], $data['password'], $data['isParty'], $data['partyData']);
- // 保存第三方用户信息
- $this->createUserOauth($this->getUserId(), $data['isParty'], $data['partyData']);
- // 记录登录态
- return $this->setSession();
- }
- /**
- * 快捷登录:微信小程序用户
- * @param array $form
- * @return bool
- * @throws BaseException
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @throws \think\Exception
- */
- public function loginMpWx(array $form): bool
- {
- // 获取微信小程序登录态(session)
- $wxSession = PartyService::getMpWxSession($form['partyData']['code']);
- // 判断openid是否存在
- $userId = OauthService::getUserIdByOauthId($wxSession['openid'], ClientEnum::MP_WEIXIN);
- // 获取用户信息
- $userInfo = !empty($userId) ? UserModel::detail($userId) : null;
- // 用户信息存在, 更新登录信息
- if (!empty($userInfo)) {
- // 更新用户登录信息
- $this->updateUser($userInfo, true, $form['partyData']);
- // 记录登录态
- return $this->setSession();
- }
- // 用户信息不存在 => 注册新用户 或者 跳转到绑定手机号页
- $setting = SettingModel::getItem(SettingEnum::REGISTER);
- // 后台设置了需强制绑定手机号, 返回前端isBindMobile, 跳转到手机号验证页
- if ($setting['isForceBindMpweixin']) {
- throwError('当前用户未绑定手机号', null, ['isBindMobile' => true, 'isPrompt' => false]);
- }
- // 后台未开启强制绑定手机号, 直接保存新用户
- if (!$setting['isForceBindMpweixin']) {
- // 用户不存在: 创建一个新用户
- $this->createUser('', true, $form['partyData']);
- // 保存第三方用户信息
- $this->createUserOauth($this->getUserId(), true, $form['partyData']);
- }
- // 记录登录态
- return $this->setSession();
- }
- /**
- * 是否需要填写昵称头像 (微信小程序端)
- * @param string $code
- * @return bool
- * @throws BaseException
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @throws \think\Exception
- */
- public function isPersonalMpweixin(string $code): bool
- {
- // 后台需开启填写微信头像和昵称
- $setting = SettingModel::getItem(SettingEnum::REGISTER);
- if (!$setting['isPersonalMpweixin']) {
- return false;
- }
- // 获取微信小程序登录态 (session)
- $wxSession = PartyService::getMpWxSession($code);
- // 判断用户是否存在 (openid)
- return !OauthService::getUserIdByOauthId($wxSession['openid'], ClientEnum::MP_WEIXIN);
- }
- /**
- * 快捷登录:微信小程序授权手机号登录
- * @param array $form
- * @return bool
- * @throws BaseException
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- * @throws \think\Exception
- */
- public function loginMpWxMobile(array $form): bool
- {
- // 获取微信小程序登录态(session)
- $wxSession = PartyService::getMpWxSession($form['code']);
- // 解密encryptedData -> 拿到手机号
- $plainData = OauthService::wxDecryptData($form['encryptedData'], $form['iv'], $wxSession['session_key']);
- // 整理登录注册数据
- $loginData = [
- 'mobile' => $plainData['purePhoneNumber'],
- 'isParty' => $form['isParty'],
- 'partyData' => $form['partyData'],
- ];
- // 自动登录注册
- $this->register($loginData);
- // 保存第三方用户信息
- $this->createUserOauth($this->getUserId(), $loginData['isParty'], $loginData['partyData']);
- // 记录登录态
- return $this->setSession();
- }
- /**
- * 保存oauth信息(第三方用户信息)
- * @param int $userId 用户ID
- * @param bool $isParty 是否为第三方用户
- * @param array $partyData 第三方用户数据
- * @return void
- * @throws BaseException
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- */
- private function createUserOauth(int $userId, bool $isParty, array $partyData = []): void
- {
- if ($isParty) {
- $Oauth = new PartyService;
- $Oauth->createUserOauth($userId, $partyData);
- }
- }
- /**
- * 当前登录的用户信息
- * @return UserModel
- */
- public function getUserInfo(): UserModel
- {
- return $this->userInfo;
- }
- /**
- * 当前登录的用户ID
- * @return int
- */
- private function getUserId(): int
- {
- return (int)$this->getUserInfo()['user_id'];
- }
- /**
- * 手机号 + 密码登录
- * 1. 用户不存在: 自动注册并设置密码 (与页面文案“未注册的手机号登录后将自动注册”一致)
- * 2. 用户存在但未设置密码: 本次输入的密码作为初始密码 (兼容历史短信登录的老用户)
- * 3. 用户已设置密码: 校验密码, 失败累计次数并锁定
- * @param string $mobile 手机号
- * @param string $password 登录密码(明文)
- * @param bool $isParty 是否存在第三方用户信息
- * @param array $partyData 第三方用户数据
- * @return void
- * @throws BaseException
- * @throws \think\Exception
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- */
- private function loginByPassword(string $mobile, string $password, bool $isParty, array $partyData = []): void
- {
- // 查询用户是否已存在
- $userInfo = UserModel::detail(['mobile' => $mobile]);
- // 用户不存在: 注册新用户并设置密码
- if (empty($userInfo)) {
- $this->createUser($mobile, $isParty, $partyData, $password);
- return;
- }
- // 用户已存在但尚未设置密码: 本次输入的密码作为初始密码
- if (!$userInfo->hasPassword()) {
- $this->updateUser($userInfo, $isParty, $partyData, $password);
- return;
- }
- // 是否已被锁定
- $this->checkLoginFailTimes($mobile);
- // 校验密码
- if (!$userInfo->verifyPassword($password)) {
- $this->recordLoginFail($mobile);
- throwError('手机号或密码不正确');
- }
- // 登录成功, 清空失败记录
- Cache::delete($this->getLoginFailCacheKey($mobile));
- // 更新用户登录信息
- $this->updateUser($userInfo, $isParty, $partyData);
- }
- /**
- * 登录失败次数缓存的key
- * @param string $mobile
- * @return string
- */
- private function getLoginFailCacheKey(string $mobile): string
- {
- return 'login_fail_' . md5("{$this->storeId}_{$mobile}");
- }
- /**
- * 检查登录失败次数 (超出限制则锁定)
- * @param string $mobile
- * @return void
- * @throws BaseException
- */
- private function checkLoginFailTimes(string $mobile): void
- {
- $times = (int)Cache::get($this->getLoginFailCacheKey($mobile), 0);
- if ($times >= self::LOGIN_FAIL_LIMIT) {
- $minutes = (int)(self::LOGIN_FAIL_LOCK_TIME / 60);
- \throwError("密码错误次数过多, 请{$minutes}分钟后再试");
- }
- }
- /**
- * 记录一次登录失败
- * @param string $mobile
- * @return void
- */
- private function recordLoginFail(string $mobile): void
- {
- $key = $this->getLoginFailCacheKey($mobile);
- $times = (int)Cache::get($key, 0) + 1;
- Cache::set($key, $times, self::LOGIN_FAIL_LOCK_TIME);
- }
- /**
- * 自动登录注册 (第三方/授权手机号登录, 无需密码)
- * @param array $data
- * @throws BaseException
- * @throws \think\Exception
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- */
- private function register(array $data): void
- {
- // 查询用户是否已存在
- // 用户存在: 更新用户登录信息
- $userInfo = UserModel::detail(['mobile' => $data['mobile']]);
- if ($userInfo) {
- $this->updateUser($userInfo, $data['isParty'], $data['partyData']);
- return;
- }
- // 用户不存在: 创建一个新用户
- $this->createUser($data['mobile'], $data['isParty'], $data['partyData']);
- }
- /**
- * 新增用户
- * @param string $mobile 手机号
- * @param bool $isParty 是否存在第三方用户信息
- * @param array $partyData 用户信息(第三方)
- * @param string $password 登录密码(明文, 为空则不设置)
- * @return void
- * @throws \think\Exception
- * @throws \think\db\exception\DataNotFoundException
- * @throws \think\db\exception\DbException
- * @throws \think\db\exception\ModelNotFoundException
- */
- private function createUser(string $mobile, bool $isParty, array $partyData = [], string $password = ''): void
- {
- // 用户信息
- $data = [
- 'mobile' => $mobile,
- 'nick_name' => !empty($mobile) ? \hide_mobile($mobile) : '',
- 'platform' => \getPlatform(),
- 'last_login_time' => \time(),
- 'store_id' => $this->storeId
- ];
- // 登录密码 (模型修改器会自动使用 password_hash 加密)
- if ($password !== '') {
- $data['password'] = $password;
- }
- // 写入用户信息(第三方)
- if ($isParty === true && !empty($partyData)) {
- $partyUserInfo = PartyService::partyUserInfo($partyData, true);
- $data = \array_merge($data, $partyUserInfo);
- }
- // 新增用户记录
- $model = new UserModel;
- $model->save($data);
- // 将微信用户昵称添加编号便于后台管理, 例如:微信用户_10001
- if (\in_array($data['nick_name'], ['微信用户', '支付宝用户'])) {
- $model->save(['nick_name' => "{$data['nick_name']}_{$model['user_id']}"]);
- }
- // 记录头像文件上传者
- if (isset($data['avatar_id']) && $data['avatar_id'] > 0) {
- UploadFileModel::setUploaderId($data['avatar_id'], (int)$model['user_id']);
- }
- // 记录用户信息
- $this->userInfo = $model;
- }
- /**
- * 更新用户登录信息
- * @param UserModel $userInfo
- * @param bool $isParty 是否存在第三方用户信息
- * @param array $partyData 用户信息(第三方)
- * @param string $password 登录密码(明文, 为空则不更新)
- */
- private function updateUser(UserModel $userInfo, bool $isParty, array $partyData = [], string $password = ''): void
- {
- // 用户信息
- $data = [
- 'last_login_time' => \time(),
- 'store_id' => $this->storeId
- ];
- // 设置/更新登录密码 (模型修改器会自动使用 password_hash 加密)
- if ($password !== '') {
- $data['password'] = $password;
- }
- // 写入用户信息(第三方)
- // 如果不需要每次登录都更新微信用户头像昵称, 下面几行代码可以屏蔽掉
- // if ($isParty === true && !empty($partyData)) {
- // $partyUserInfo = PartyService::partyUserInfo($partyData);
- // $data = \array_merge($data, $partyUserInfo);
- // }
- // // 记录头像文件上传者
- // if (isset($data['avatar_id']) && $data['avatar_id'] > 0) {
- // UploadFileModel::setUploaderId($data['avatar_id'], $userInfo['user_id']);
- // }
- // 更新用户记录
- $userInfo->save($data);
- // 记录用户信息
- $this->userInfo = $userInfo;
- }
- /**
- * 记录登录态
- * @return bool
- * @throws BaseException
- */
- private function setSession(): bool
- {
- empty($this->userInfo) && \throwError('未找到用户信息');
- // 登录的token
- $token = $this->getToken($this->getUserId());
- // 记录缓存, 30天
- Cache::set($token, [
- 'user' => $this->userInfo,
- 'store_id' => $this->storeId,
- 'is_login' => true,
- ], 86400 * 30);
- return true;
- }
- /**
- * 数据验证
- * @param array $data
- * @return void
- * @throws BaseException
- */
- private function validate(array $data): void
- {
- // 数据验证 (手机号 + 登录密码)
- $validate = new ValidateLogin;
- if (!$validate->check($data)) {
- throwError($validate->getError());
- }
- }
- /**
- * 获取登录的token
- * @param int $userId
- * @return string
- */
- public function getToken(int $userId): string
- {
- static $token = '';
- if (empty($token)) {
- $token = $this->makeToken($userId);
- }
- return $token;
- }
- /**
- * 生成用户认证的token
- * @param int $userId
- * @return string
- */
- private function makeToken(int $userId): string
- {
- $storeId = $this->storeId;
- // 生成一个不会重复的随机字符串
- $guid = \get_guid_v4();
- // 当前时间戳 (精确到毫秒)
- $timeStamp = \microtime(true);
- // 自定义一个盐
- $salt = self::TOKEN_SALT;
- return md5("{$storeId}_{$timeStamp}_{$userId}_{$guid}_{$salt}");
- }
- }
|