Files
YwxAppThink/addon/wxchat/controller/api/Login.php
T

399 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
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 <admin@ywxapp.cn>
*/
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]);
}
}
/**
* 签发 JWTaccess + 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) {
// 注册日志表可能未初始化,不影响主流程
}
}
}