Login.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2017~2024 https://www.yiovo.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
  8. // +----------------------------------------------------------------------
  9. // | Author: 萤火科技 <admin@yiovo.com>
  10. // +----------------------------------------------------------------------
  11. declare (strict_types=1);
  12. namespace app\api\service\passport;
  13. use think\facade\Cache;
  14. use yiovo\captcha\facade\CaptchaApi;
  15. use app\api\model\{
  16. User as UserModel,
  17. Setting as SettingModel,
  18. UploadFile as UploadFileModel
  19. };
  20. use app\api\service\{user\Oauth as OauthService, passport\Party as PartyService};
  21. use app\api\validate\passport\Login as ValidateLogin;
  22. use app\common\service\BaseService;
  23. use app\common\enum\Client as ClientEnum;
  24. use app\common\enum\Setting as SettingEnum;
  25. use cores\exception\BaseException;
  26. /**
  27. * 服务类:用户登录
  28. * Class Login
  29. * @package app\api\service\passport
  30. */
  31. class Login extends BaseService
  32. {
  33. /**
  34. * 用户信息 (登录成功后才记录)
  35. * @var UserModel|null $userInfo
  36. */
  37. private ?UserModel $userInfo;
  38. // 用于生成token的自定义盐
  39. const TOKEN_SALT = 'user_salt';
  40. /**
  41. * 执行用户登录
  42. * @param array $data
  43. * @return bool
  44. * @throws BaseException
  45. * @throws \think\Exception
  46. * @throws \think\db\exception\DataNotFoundException
  47. * @throws \think\db\exception\DbException
  48. * @throws \think\db\exception\ModelNotFoundException
  49. */
  50. public function login(array $data): bool
  51. {
  52. // 数据验证
  53. $this->validate($data);
  54. // 自动登录注册
  55. $this->register($data);
  56. // 保存第三方用户信息
  57. $this->createUserOauth($this->getUserId(), $data['isParty'] ?? false, $data['partyData']?? []);
  58. // 记录登录态
  59. return $this->setSession();
  60. }
  61. /**
  62. * 快捷登录:微信小程序用户
  63. * @param array $form
  64. * @return bool
  65. * @throws BaseException
  66. * @throws \think\db\exception\DataNotFoundException
  67. * @throws \think\db\exception\DbException
  68. * @throws \think\db\exception\ModelNotFoundException
  69. * @throws \think\Exception
  70. */
  71. public function loginMpWx(array $form): bool
  72. {
  73. // 获取微信小程序登录态(session)
  74. $wxSession = PartyService::getMpWxSession($form['partyData']['code']);
  75. // 判断openid是否存在
  76. $userId = OauthService::getUserIdByOauthId($wxSession['openid'], ClientEnum::MP_WEIXIN);
  77. // 获取用户信息
  78. $userInfo = !empty($userId) ? UserModel::detail($userId) : null;
  79. // 用户信息存在, 更新登录信息
  80. if (!empty($userInfo)) {
  81. // 更新用户登录信息
  82. $this->updateUser($userInfo, true, $form['partyData']);
  83. // 记录登录态
  84. return $this->setSession();
  85. }
  86. // 用户信息不存在 => 注册新用户 或者 跳转到绑定手机号页
  87. $setting = SettingModel::getItem(SettingEnum::REGISTER);
  88. // 后台设置了需强制绑定手机号, 返回前端isBindMobile, 跳转到手机号验证页
  89. if ($setting['isForceBindMpweixin']) {
  90. throwError('当前用户未绑定手机号', null, ['isBindMobile' => true]);
  91. }
  92. // 后台未开启强制绑定手机号, 直接保存新用户
  93. if (!$setting['isForceBindMpweixin']) {
  94. // 用户不存在: 创建一个新用户
  95. $this->createUser('', true, $form['partyData']);
  96. // 保存第三方用户信息
  97. $this->createUserOauth($this->getUserId(), true, $form['partyData']);
  98. }
  99. // 记录登录态
  100. return $this->setSession();
  101. }
  102. /**
  103. * 是否需要填写昵称头像 (微信小程序端)
  104. * @param string $code
  105. * @return bool
  106. * @throws BaseException
  107. * @throws \think\db\exception\DataNotFoundException
  108. * @throws \think\db\exception\DbException
  109. * @throws \think\db\exception\ModelNotFoundException
  110. * @throws \think\Exception
  111. */
  112. public function isPersonalMpweixin(string $code): bool
  113. {
  114. // 后台需开启填写微信头像和昵称
  115. $setting = SettingModel::getItem(SettingEnum::REGISTER);
  116. if (!$setting['isPersonalMpweixin']) {
  117. return false;
  118. }
  119. // 获取微信小程序登录态 (session)
  120. $wxSession = PartyService::getMpWxSession($code);
  121. // 判断用户是否存在 (openid)
  122. return !OauthService::getUserIdByOauthId($wxSession['openid'], ClientEnum::MP_WEIXIN);
  123. }
  124. /**
  125. * 快捷登录:微信小程序用户
  126. * @param array $form
  127. * @return bool
  128. * @throws BaseException
  129. * @throws \think\db\exception\DataNotFoundException
  130. * @throws \think\db\exception\DbException
  131. * @throws \think\db\exception\ModelNotFoundException
  132. * @throws \think\Exception
  133. */
  134. public function loginMpWxMobile(array $form): bool
  135. {
  136. // 获取微信小程序登录态(session)
  137. $wxSession = PartyService::getMpWxSession($form['code']);
  138. // 解密encryptedData -> 拿到手机号
  139. $plainData = OauthService::wxDecryptData($form['encryptedData'], $form['iv'], $wxSession['session_key']);
  140. // 整理登录注册数据
  141. $loginData = [
  142. 'mobile' => $plainData['purePhoneNumber'],
  143. 'isParty' => $form['isParty'],
  144. 'partyData' => $form['partyData'],
  145. ];
  146. // 自动登录注册
  147. $this->register($loginData);
  148. // 保存第三方用户信息
  149. $this->createUserOauth($this->getUserId(), $loginData['isParty'], $loginData['partyData']);
  150. // 记录登录态
  151. return $this->setSession();
  152. }
  153. /**
  154. * 保存oauth信息(第三方用户信息)
  155. * @param int $userId 用户ID
  156. * @param bool $isParty 是否为第三方用户
  157. * @param array $partyData 第三方用户数据
  158. * @return void
  159. * @throws BaseException
  160. * @throws \think\db\exception\DataNotFoundException
  161. * @throws \think\db\exception\DbException
  162. * @throws \think\db\exception\ModelNotFoundException
  163. */
  164. private function createUserOauth(int $userId, bool $isParty, array $partyData = []): void
  165. {
  166. if ($isParty) {
  167. $Oauth = new PartyService;
  168. $Oauth->createUserOauth($userId, $partyData);
  169. }
  170. }
  171. /**
  172. * 当前登录的用户信息
  173. * @return UserModel
  174. */
  175. public function getUserInfo(): UserModel
  176. {
  177. return $this->userInfo;
  178. }
  179. /**
  180. * 当前登录的用户ID
  181. * @return int
  182. */
  183. private function getUserId(): int
  184. {
  185. return (int)$this->getUserInfo()['user_id'];
  186. }
  187. /**
  188. * 自动登录注册
  189. * @param array $data
  190. * @throws BaseException
  191. * @throws \think\Exception
  192. * @throws \think\db\exception\DataNotFoundException
  193. * @throws \think\db\exception\DbException
  194. * @throws \think\db\exception\ModelNotFoundException
  195. */
  196. private function register(array $data): void
  197. {
  198. // 查询用户是否已存在
  199. // 用户存在: 更新用户登录信息
  200. $userInfo = UserModel::detail(['mobile' => $data['mobile']]);
  201. if ($userInfo) {
  202. $this->updateUser($userInfo, $data['isParty'], $data['partyData']);
  203. return;
  204. }
  205. // 用户不存在: 创建一个新用户
  206. $this->createUser($data['mobile'], $data['isParty'], $data['partyData']);
  207. }
  208. /**
  209. * 新增用户
  210. * @param string $mobile 手机号
  211. * @param bool $isParty 是否存在第三方用户信息
  212. * @param array $partyData 用户信息(第三方)
  213. * @return void
  214. * @throws \think\Exception
  215. * @throws \think\db\exception\DataNotFoundException
  216. * @throws \think\db\exception\DbException
  217. * @throws \think\db\exception\ModelNotFoundException
  218. */
  219. private function createUser(string $mobile, bool $isParty, array $partyData = []): void
  220. {
  221. // 用户信息
  222. $data = [
  223. 'mobile' => $mobile,
  224. 'nick_name' => !empty($mobile) ? \hide_mobile($mobile) : '',
  225. 'platform' => \getPlatform(),
  226. 'last_login_time' => \time(),
  227. 'store_id' => $this->storeId
  228. ];
  229. // 写入用户信息(第三方)
  230. if ($isParty === true && !empty($partyData)) {
  231. $partyUserInfo = PartyService::partyUserInfo($partyData, true);
  232. $data = array_merge($data, $partyUserInfo);
  233. }
  234. // 新增用户记录
  235. $model = new UserModel;
  236. $model->save($data);
  237. // 将微信用户昵称添加编号便于后台管理, 例如:微信用户_10001
  238. if (\in_array($data['nick_name'], ['微信用户', '支付宝用户'])) {
  239. $model->save(['nick_name' => "{$data['nick_name']}_{$model['user_id']}"]);
  240. }
  241. // 记录头像文件上传者
  242. if (isset($data['avatar_id']) && $data['avatar_id'] > 0) {
  243. UploadFileModel::setUploaderId($data['avatar_id'], (int)$model['user_id']);
  244. }
  245. // 记录用户信息
  246. $this->userInfo = $model;
  247. }
  248. /**
  249. * 更新用户登录信息
  250. * @param UserModel $userInfo
  251. * @param bool $isParty 是否存在第三方用户信息
  252. * @param array $partyData 用户信息(第三方)
  253. */
  254. private function updateUser(UserModel $userInfo, bool $isParty, array $partyData = []): void
  255. {
  256. // 用户信息
  257. $data = [
  258. 'last_login_time' => \time(),
  259. 'store_id' => $this->storeId
  260. ];
  261. // 写入用户信息(第三方)
  262. // 如果不需要每次登录都更新微信用户头像昵称, 下面几行代码可以屏蔽掉
  263. // if ($isParty === true && !empty($partyData)) {
  264. // $partyUserInfo = PartyService::partyUserInfo($partyData);
  265. // $data = array_merge($data, $partyUserInfo);
  266. // }
  267. // // 记录头像文件上传者
  268. // if (isset($data['avatar_id']) && $data['avatar_id'] > 0) {
  269. // UploadFileModel::setUploaderId($data['avatar_id'], $userInfo['user_id']);
  270. // }
  271. // 更新用户记录
  272. $userInfo->save($data);
  273. // 记录用户信息
  274. $this->userInfo = $userInfo;
  275. }
  276. /**
  277. * 记录登录态
  278. * @return bool
  279. * @throws BaseException
  280. */
  281. private function setSession(): bool
  282. {
  283. empty($this->userInfo) && \throwError('未找到用户信息');
  284. // 登录的token
  285. $token = $this->getToken($this->getUserId());
  286. // 记录缓存, 30天
  287. Cache::set($token, [
  288. 'user' => $this->userInfo,
  289. 'store_id' => $this->storeId,
  290. 'is_login' => true,
  291. ], 86400 * 30);
  292. return true;
  293. }
  294. /**
  295. * 数据验证
  296. * @param array $data
  297. * @return void
  298. * @throws BaseException
  299. */
  300. private function validate(array $data): void
  301. {
  302. // 数据验证
  303. $validate = new ValidateLogin;
  304. if (!$validate->check($data)) {
  305. throwError($validate->getError());
  306. }
  307. // 验证短信验证码是否匹配
  308. $mailCaptcha = new MailCaptcha();
  309. try {
  310. $mailCaptcha->checkCaptcha($data['smsCode'], $data['mobile']);
  311. //CaptchaApi::checkSms($data['smsCode'], $data['mobile']);
  312. } catch (\Exception $e) {
  313. throwError($e->getMessage() ?: '短信验证码不正确');
  314. }
  315. }
  316. /**
  317. * 获取登录的token
  318. * @param int $userId
  319. * @return string
  320. */
  321. public function getToken(int $userId): string
  322. {
  323. static $token = '';
  324. if (empty($token)) {
  325. $token = $this->makeToken($userId);
  326. }
  327. return $token;
  328. }
  329. /**
  330. * 生成用户认证的token
  331. * @param int $userId
  332. * @return string
  333. */
  334. private function makeToken(int $userId): string
  335. {
  336. $storeId = $this->storeId;
  337. // 生成一个不会重复的随机字符串
  338. $guid = \get_guid_v4();
  339. // 当前时间戳 (精确到毫秒)
  340. $timeStamp = \microtime(true);
  341. // 自定义一个盐
  342. $salt = self::TOKEN_SALT;
  343. return md5("{$storeId}_{$timeStamp}_{$userId}_{$guid}_{$salt}");
  344. }
  345. }