// +---------------------------------------------------------------------- declare (strict_types = 1); namespace addon\wxchat\controller\api; use think\facade\Request; use ywxapp\controller\FrontendBase; use ywxapp\utils\HttpClient; use think\facade\Db; use think\facade\Cache; use ywxapp\service\JwtService; /** * Login 类 * * @author ywxapp */ class Login extends FrontendBase { /** * Summary of noNeedLogin * @var array */ protected $noNeedLogin = ['*']; /** * Summary of noNeedVerify * @var array */ protected $noNeedVerify = ['*']; /** * 控制器初始化 initialize * @return void */ protected function initialize() {} public function index() { echo "test"; } // 短信验证码登录(验证码校验通过后,查找或自动注册用户,并签发 JWT) public function smsLogin() { try { $mobile = $this->request->post("mobile"); $event = $this->request->post("event", 'login'); $captcha = $this->request->post("captcha"); $data = ['mobile' => $mobile, 'captcha' => $captcha, 'event' => $event]; validate(\addon\wxchat\validate\Login::class) ->scene('sms') ->check($data); $ret = app('sms')->check($mobile, $captcha, $event); if (! $ret) { $this->error("验证码不正确!"); } $user = Db::name('member')->where('mobile', $mobile)->where('status', '>=', 0)->find(); if (empty($user)) { $uid = Db::name('member')->insertGetId([ 'account' => $mobile, 'mobile' => $mobile, 'nickname' => $this->generateRandomNickname(), 'password' => '', 'status' => 1, 'create_at' => time(), 'create_ip' => Request::ip(), 'update_at' => time(), 'update_ip' => Request::ip(), ]); $user = Db::name('member')->find($uid); } else { Db::name('member')->where('uid', $user['uid'])->update([ 'update_at' => time(), 'update_ip' => Request::ip(), ]); } $this->issueToken((int) $user['uid']); $this->success([ 'user_info' => $this->formatUserInfo($user), 'is_registered' => true, ], '登录成功'); } catch (\think\exception\ValidateException $e) { $this->error($e->getMessage(), 400); } catch (\Exception $e) { $this->error('系统错误:' . $e->getMessage(), 500); } } // 一键登录(本机号码):用预下发的一次性 pre_token 换取登录态 // 流程:客户端先调用 getOneClickPreToken(phone) 拿到 pre_token(模拟运营商 SDK 下发的本机号码凭证), // 再携带 pre_token 调用本接口,服务端校验后签发 JWT。 public function oneClickLogin() { try { $preToken = $this->request->post('pre_token'); if (empty($preToken)) { return json(['code' => 400, 'msg' => '缺少一键登录凭证', 'data' => null]); } $phone = Cache::get('wxchat_oneclick_' . $preToken); if (empty($phone)) { return json(['code' => 400, 'msg' => '一键登录凭证已失效,请重试', 'data' => null]); } Cache::delete('wxchat_oneclick_' . $preToken); $user = Db::name('member')->where('mobile', $phone)->where('status', '>=', 0)->find(); if (empty($user)) { $uid = Db::name('member')->insertGetId([ 'account' => $phone, 'mobile' => $phone, 'nickname' => $this->generateRandomNickname(), 'password' => '', 'status' => 1, 'create_at' => time(), 'create_ip' => Request::ip(), 'update_at' => time(), 'update_ip' => Request::ip(), ]); $user = Db::name('member')->find($uid); } else { Db::name('member')->where('uid', $user['uid'])->update([ 'update_at' => time(), 'update_ip' => Request::ip(), ]); } $this->issueToken((int) $user['uid']); $this->success([ 'user_info' => $this->formatUserInfo($user), 'is_registered' => true, ], '登录成功'); } catch (\think\exception\ValidateException $e) { return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]); } catch (\Exception $e) { return json(['code' => 500, 'msg' => '系统错误:' . $e->getMessage(), 'data' => null]); } } // 完善个人信息(注册)。仅写入 user 表真实存在的字段。 public function completeUserInfo() { try { $data = Request::only([ 'temp_token', 'nickname', 'avatar', ], 'post'); // 验证临时token $tempData = Cache::get('temp_login_' . ($data['temp_token'] ?? '')); if (! $tempData || ($tempData['expire_time'] ?? 0) < time()) { return json(['code' => 401, 'msg' => '临时凭证已过期,请重新登录', 'data' => null]); } if (empty($tempData['verified'])) { return json(['code' => 403, 'msg' => '手机号未验证', 'data' => null]); } $phone = $tempData['phone']; // 检查手机号是否已被注册 $exists = Db::name('member') ->where('mobile', $phone) ->where('status', 1) ->find(); if ($exists) { return json(['code' => 400, 'msg' => '该手机号已注册', 'data' => null]); } // 处理头像上传 $avatarPath = ''; if (! empty($data['avatar'])) { $avatarPath = $this->handleAvatarUpload($data['avatar'], $phone); } $nickname = ! empty($data['nickname']) ? $data['nickname'] : $this->generateRandomNickname(); $userData = [ 'account' => $phone, 'mobile' => $phone, 'nickname' => $nickname, 'avatar' => $avatarPath, 'password' => '', 'status' => 1, 'create_at' => time(), 'create_ip' => Request::ip(), 'update_at' => time(), 'update_ip' => Request::ip(), ]; $userId = Db::name('member')->insertGetId($userData); if (! $userId) { throw new \Exception('用户注册失败'); } Cache::delete('temp_login_' . ($data['temp_token'] ?? '')); $this->issueToken($userId); $this->logUserRegister($userId, $userData); $user = Db::name('member')->find($userId); $this->success([ 'user_info' => $this->formatUserInfo($user), 'is_registered' => true, ], '注册成功'); } catch (\think\exception\ValidateException $e) { return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]); } catch (\Exception $e) { return json(['code' => 500, 'msg' => '系统错误:' . $e->getMessage(), 'data' => null]); } } // 刷新 Access Token(无感续期):校验 refresh_token,重签 access 并轮转 refresh public function refresh() { try { $refreshToken = $this->request->post('refresh_token'); if (empty($refreshToken)) { $this->error('缺少刷新令牌', 400); } $jwt = JwtService::instance(); $token = $jwt->parseRefreshToken($refreshToken); $claims = $token->claims()->all(); $uid = (int) ($claims['uid'] ?? 0); if ($uid <= 0) { $this->error('刷新令牌无效', 400); } // 重新签发 access token,并轮转 refresh token $jwt->createAccessToken(['uid' => $uid]); $jwt->createRefreshToken(['uid' => $uid]); $this->success([], '刷新成功'); } catch (\Exception $e) { $this->error('刷新令牌已失效,请重新登录', 401); } } // 下发一键登录预凭证(模拟运营商 SDK 下发的本机号码凭证) public function getOneClickPreToken() { try { $phone = $this->request->post('phone'); if (! preg_match('/^1[3-9]\d{9}$/', (string) $phone)) { $this->error('手机号格式错误', 400); } $preToken = md5(uniqid((string) microtime(true), true) . $phone . time()); Cache::set('wxchat_oneclick_' . $preToken, $phone, 300); $this->success(['pre_token' => $preToken]); } catch (\Exception $e) { $this->error('系统错误:' . $e->getMessage(), 500); } } // 发送短信验证码 public function sendSmsCode() { try { $phone = Request::param('phone'); // 验证手机号格式 if (! preg_match('/^1[3-9]\d{9}$/', $phone)) { return json(['code' => 400, 'msg' => '手机号格式错误', 'data' => null]); } // 检查发送频率 $cacheKey = 'sms_limit_' . $phone; $lastSendTime = Cache::get($cacheKey); if ($lastSendTime && time() - $lastSendTime < 60) { return json(['code' => 400, 'msg' => '请稍后再试', 'data' => null]); } // 生成6位随机验证码 $code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT); // 调用短信服务 $smsService = new SmsService(); $sendResult = $smsService->sendLoginCode($phone, $code); if ($sendResult) { // 记录发送时间 Cache::set($cacheKey, time(), 60); return json([ 'code' => 200, 'msg' => '验证码发送成功', 'data' => [ 'expire_time' => 300, // 5分钟有效期 ], ]); } else { return json(['code' => 500, 'msg' => '验证码发送失败', 'data' => null]); } } catch (\Exception $e) { return json(['code' => 500, 'msg' => '系统错误:' . $e->getMessage(), 'data' => null]); } } /** * 签发 JWT(access + refresh),借助 JwtService 内部写入 app()->result, * 最终由 $this->success() 在响应顶层输出 access_token/refresh_token/... */ private function issueToken(int $uid) : void { JwtService::instance()->createToken(['uid' => $uid]); } /** * 统一输出用户信息(对齐客户端 IUserInfo 字段) */ private function formatUserInfo(array $user) : array { return [ 'uid' => (int) ($user['uid'] ?? 0), 'account' => $user['account'] ?? '', 'nickname' => $user['nickname'] ?? '', 'avatar' => $user['avatar'] ?? '', 'mobile' => $user['mobile'] ?? '', 'status' => (int) ($user['status'] ?? 1), 'create_time' => $user['create_at'] ?? 0, ]; } // 处理头像上传 private function handleAvatarUpload($base64Image, $phone) { try { if (strpos($base64Image, 'data:image') === 0) { // Base64图片 $imageData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $base64Image)); // 生成文件名 $fileName = 'avatar_' . $phone . '_' . time() . '.jpg'; $filePath = 'uploads/avatar/' . date('Ymd') . '/' . $fileName; // 保存文件 $savePath = public_path() . $filePath; $dir = dirname($savePath); if (! is_dir($dir)) { mkdir($dir, 0755, true); } file_put_contents($savePath, $imageData); return $filePath; } else { // URL或其他格式 return $base64Image; } } catch (\Exception $e) { // 头像上传失败不影响注册 return ''; } } // 生成随机昵称 private function generateRandomNickname() { $adjectives = ['快乐的', '聪明的', '勇敢的', '温柔的', '活泼的', '安静的', '热情的', '冷静的']; $nouns = ['小猫', '小狗', '小鸟', '小鱼', '小虎', '小兔', '小熊', '小鹿']; $numbers = ['123', '456', '789', '007', '888', '999']; $adj = $adjectives[array_rand($adjectives)]; $noun = $nouns[array_rand($nouns)]; $num = $numbers[array_rand($numbers)]; return $adj . $noun . $num; } // 记录用户注册日志 private function logUserRegister($userId, $userData) { $logData = [ 'user_id' => $userId, 'mobile' => $userData['mobile'] ?? '', 'register_time' => date('Y-m-d H:i:s'), 'register_ip' => Request::ip(), 'invite_user_id' => $userData['invite_user_id'] ?? 0, 'create_at' => time(), ]; try { Db::name('user_register_log')->insert($logData); } catch (\Exception $e) { // 注册日志表可能未初始化,不影响主流程 } } }