399 lines
14 KiB
PHP
399 lines
14 KiB
PHP
<?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]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 签发 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) {
|
||
// 注册日志表可能未初始化,不影响主流程
|
||
}
|
||
}
|
||
}
|