feat : login wx

This commit is contained in:
2024-11-11 17:04:10 +08:00
parent 83a38d5001
commit 3a5739ee19
20 changed files with 844 additions and 4 deletions

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Aspect\Api;
use App\Extend\SystemUtil;
use App\Lib\ApiReturn;
use App\Lib\Crypto\CryptoFactory;
use App\Lib\Log;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Di\Exception\Exception;
use Hyperf\HttpServer\Contract\RequestInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
#[
Aspect(
classes: [
ApiReturn::class,
],
annotations: []
)
]
class ApiReturnAspect extends AbstractAspect
{
/**
* @param RequestInterface $request
* @param CryptoFactory $CryptoFactory
* @param Log $log
* @param int $userId
*/
public function __construct(
private readonly RequestInterface $request,
private readonly CryptoFactory $CryptoFactory,
private readonly Log $log,
private int $userId = 0
){}
/**
* @param ProceedingJoinPoint $proceedingJoinPoint
* @return mixed
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function process(ProceedingJoinPoint $proceedingJoinPoint): mixed
{
// 在调用前进行处理
$result = $proceedingJoinPoint->process();
// 在调用后进行处理
$this->userId = Context::get('user_id',0);
// 加密前写请求日志
$this->writeResponseLog(json_encode($result));
//正式服加密 测试服不做处理
if (SystemUtil::checkProEnv()) {
$cryptoFactory = $this->CryptoFactory->cryptoClass('api', json_encode($result['data']));
$result['data'] = $cryptoFactory->encrypt();
}
return $result;
}
/**
* 请求日志
* @param string $result
* @return void
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function writeResponseLog(string $result): void
{
$post = $this->request->all();
$logParam = !$post ? '{}' : json_encode($post);
$header = json_encode($this->request->getHeaders());
$this->log->requestApiLog("\napi==path:{$this->request->getPathInfo()}\nuserId:$this->userId\nrequestData:$logParam\nheader:$header\nresponseData:$result");
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Cache\Redis\Api;
class ApiRedisKey
{
/**
* 注册/登录的锁
* @param string $mobile
* @return string
*/
public static function LoginAndRegisterByMobileLock(string $mobile): string
{
return 'login:or:register:mobile:'.$mobile;
}
/**
* 注册/登录的锁
* @param string $code
* @return string
*/
public static function LoginAndRegisterByCodeLock(string $code): string
{
return 'login:or:register:code:'.$code;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Constants\Common;
class UserCode
{
/**
* 是否绑定手机号 1=是 2=否
*/
const int IS_BIND_PHONE = 1;
const int IS_NOT_BIND_PHONE = 2;
/**
* 是否默认头像 1=不是 2=是
*/
const int IS_NOT_DEFAULT_AVATAR = 1;
const int IS_DEFAULT_AVATAR = 2;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api;
use App\Request\Api\LoginRequest;
use App\Service\Api\Login\LoginService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Validation\Annotation\Scene;
#[Controller(prefix: 'api/login')]
class LoginController
{
#[RequestMapping(path: 'index',methods: 'post')]
#[Scene(scene: 'login')]
public function index(LoginRequest $request)
{
return (new LoginService)->handle();
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Exception;
use Hyperf\Server\Exception\ServerException;
class ApiException extends ServerException
{
}

View File

@@ -11,9 +11,11 @@ use Throwable;
class AdminExceptionHandler extends ExceptionHandler class AdminExceptionHandler extends ExceptionHandler
{ {
public function __construct(protected AdminReturn $return) /**
{ * 注入
} * @param AdminReturn $return
*/
public function __construct(private readonly AdminReturn $return) {}
/** /**
* admin控制器异常处理 * admin控制器异常处理

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Exception\Handler;
use App\Exception\ApiException;
use App\Lib\ApiReturn;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Swow\Psr7\Message\ResponsePlusInterface;
use Throwable;
class ApiExceptionHandler extends ExceptionHandler
{
/**
* 注入
* @param ApiReturn $return
*/
public function __construct(private readonly ApiReturn $return) {}
/**
* @param Throwable $throwable
* @param ResponsePlusInterface $response
* @return ResponsePlusInterface
*/
public function handle(Throwable $throwable, ResponsePlusInterface $response): ResponsePlusInterface
{
if ($throwable instanceof ApiException) {
$result = $this->return->error($throwable->getMessage(),$throwable->getCode());
// 阻止异常冒泡
$this->stopPropagation();
return $response->withHeader("Content-Type", "application/json")
->withStatus(200)
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
}
// 交给下一个异常处理器
return $response;
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

40
app/Lib/ApiReturn.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
namespace App\Lib;
use App\Constants\ReturnCode;
class ApiReturn
{
/**
* 通用api返回
* @param string $msg
* @param array $data
* @param int $code
* @return array
*/
public function success(string $msg = 'success', array $data = [], int $code = ReturnCode::SUCCESS): array
{
return [
'code' => $code,
'msg' => $msg,
'data' => $data
];
}
/**
* 通用api返回
* @param string $msg
* @param array $data
* @param int $code
* @return array
*/
public function error(string $msg = 'error', int $code = ReturnCode::ERROR, array $data = []): array
{
return [
'code' => $code,
'msg' => $msg,
'data' => $data
];
}
}

51
app/Model/User.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Hyperf\DbConnection\Model\Model;
/**
* @property int $id
* @property string $mobile
* @property string $nickname
* @property string $password
* @property string $salt
* @property int $avatar_id
* @property int $gender
* @property int $age
* @property string $birthday
* @property int $city
* @property string $token
* @property string $last_ip
* @property string $reg_ip
* @property string $last_time
* @property string $create_time
* @property string $update_time
* @property int $is_del
*/
class User extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'user';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
protected array $guarded = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'avatar_id' => 'integer', 'gender' => 'integer', 'age' => 'integer', 'city' => 'integer', 'is_del' => 'integer'];
const CREATED_AT = 'create_time';
const UPDATED_AT = 'update_time';
}

43
app/Model/UserThird.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Hyperf\Database\Model\Builder;
use Hyperf\DbConnection\Model\Model;
/**
* @property int $id
* @property int $user_id
* @property string $open_id
* @property int $type
* @property string $union_id
*/
class UserThird extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'user_third';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
protected array $guarded = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'user_id' => 'integer', 'type' => 'integer'];
/**
* @param string $openId
* @return Builder|\Hyperf\Database\Model\Model|null
*/
public function getThirdInfoByOpenId(string $openId): \Hyperf\Database\Model\Model|Builder|null
{
return $this->where('open_id', $openId)->first();
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Request\Api;
use Hyperf\Validation\Request\FormRequest;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Api;
use App\Lib\ApiReturn;
use Hyperf\Context\Context;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
abstract class BaseService
{
/**
* 请求对象
* @var RequestInterface
*/
#[Inject]
protected RequestInterface $request;
/**
* 通用返回对象
* @var ApiReturn $return
*/
#[Inject]
protected ApiReturn $return;
/**
* 用户id
* @var int $userId
*/
protected int $userId = 0;
/**
* 主构造函数(获取请求对象)
*/
public function __construct()
{
$this->userId = Context::get("user_id",0);
}
/**
* 主体函数抽象类
*/
abstract public function handle();
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Api\Login;
use App\Cache\Redis\Api\ApiRedisKey;
use App\Cache\Redis\RedisCache;
use App\Constants\Common\UserCode;
use App\Exception\ApiException;
use App\Extend\StringUtil;
use App\Extend\SystemUtil;
use App\Lib\Crypto\CryptoFactory;
use App\Model\User;
use App\Model\UserThird;
use App\Service\Api\BaseService;
use Hyperf\Di\Annotation\Inject;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
abstract class LoginBaseService extends BaseService
{
/**
* 注入缓存类
* @var RedisCache
*/
#[Inject]
protected RedisCache $redis;
/**
* 注入加密工厂
* @var CryptoFactory
*/
#[Inject]
protected CryptoFactory $cryptoFactory;
/**
* 注入三方用户
* @var UserThird
*/
#[Inject]
protected UserThird $userThirdModel;
/**
* 锁定注册
* @var string
*/
protected string $lockKey;
/**
* 登录code login_type = wx_login 必填
* @var string
*/
protected string $jsCode = '';
/**
* 手机号 login_type = mobile_code 必填
* @var string
*/
protected string $mobile = '';
/**
* 获取的用户信息
* @var
*/
protected mixed $userInfo;
/**
* @var string
*/
protected string $openId = '';
/**
* @var string
*/
protected string $unionId = '';
/**
* 登录
* @return void
*/
protected function login(): void
{
$this->userId = empty($this->userId) ? $this->userInfo->id : $this->userId;
if (empty($this->userId)) {
throw new ApiException('登录失败');
}
//todo 判断注销 判断封号
//todo 更新登录时间
}
/**
* 返回值
* @return array
* @throws \Exception
*/
protected function getReturn():array
{
$loginReturn = [
'id' => $this->userId,
'is_bind_mobile' => $this->userInfo->mobile ? UserCode::IS_BIND_PHONE : UserCode::IS_NOT_BIND_PHONE,
'nickName' => $this->userInfo->nickName,
'is_default_avatar' => $this->userInfo->avatar_id == 0 ? UserCode::IS_DEFAULT_AVATAR : UserCode::IS_NOT_DEFAULT_AVATAR,
];
$loginReturn['token'] = $this->cryptoFactory->cryptoClass('jwt', json_encode($loginReturn))->encrypt();
return $loginReturn;
}
/**
* 判断是不是没有加锁
* @param $type
* @return void
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws \RedisException
*/
protected function checkLock($type): void
{
$this->lockKey = match ($type){
1 => ApiRedisKey::LoginAndRegisterByMobileLock($this->mobile),
2 => ApiRedisKey::LoginAndRegisterByCodeLock($this->jsCode)
};
if (0 == ($this->redis->addLock($this->lockKey))){
throw new ApiException('请勿重复点击');
}
}
/**
* 添加用户
* @return void
*/
protected function addUser(): void
{
$model = new User();
//默认头像和默认名称
$model->nickname = '用户'.StringUtil::randStr(6);
$model->avatar_id = 0;
$model->reg_ip = SystemUtil::getClientIp();
if (!$model->save()) throw new ApiException('数据保存失败-注册失败');
$this->userId = $model->id;
$this->userInfo = $model;
}
abstract protected function register();
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Api\Login;
use App\Exception\AdminException;
use App\Service\Api\BaseService;
class LoginService extends BaseService
{
protected string $loginType = '';
public function handle()
{
$this->loginType = $this->request->input('login_type','');
$factory = new LoginTypeFactory();
$service = match ($this->request->input('login_type',''))
{
'wx_login' => $factory->wxFastLogin(),
'mobile_code' => $factory->mobileCodeLogin(),
default => throw new AdminException('登录类型错误'),
};
$loginInfo = $service->handle();
return $this->return->success('登录成功',$loginInfo);
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Api\Login;
class LoginTypeFactory
{
/**
* 验证码登录
* @return MobileCodeLoginService
*/
public function mobileCodeLogin(): MobileCodeLoginService
{
return new MobileCodeLoginService();
}
/**
* 微信快速组件登录
* @return WxFastLoginService
*/
public function wxFastLogin(): WxFastLoginService
{
return new WxFastLoginService();
}
}

View File

@@ -0,0 +1,23 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Api\Login;
class MobileCodeLoginService extends LoginBaseService
{
public function handle()
{
return [];
}
protected function register() {}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\Api\Login;
use App\Exception\ApiException;
use App\Model\UserThird;
use App\Service\ServiceTrait\Api\WxMiniTrait;
use Hyperf\DbConnection\Db;
class WxFastLoginService extends LoginBaseService
{
use WxMiniTrait;
/**
* 登录类型 2=微信小程序
*/
const int LOGIN_TYPE = 2;
public function handle()
{
$this->jsCode = $this->request->input('js_code');
if (empty($this->jsCode)) throw new ApiException('登录参数错误');
$wxInfo = $this->jsCodeGetOpenId($this->jsCode);
$this->openId = $wxInfo['openid'];
$this->unionId = $wxInfo['unionid'];
$this->checkLock(self::LOGIN_TYPE);
$this->register();
$this->login();
$this->redis->delLock($this->lockKey);
return $this->getReturn();
}
protected function register(): void
{
$thirdInfo = $this->userThirdModel->getThirdInfoByOpenId($this->openId);
if (!empty($thirdInfo)) {
$this->userId = $thirdInfo->user_id;
return;
}
// todo 设备封禁不可注册 注册限制
Db::transaction(function () {
$this->addUser();
$this->addUserThird();
//todo 要不要生成邀请码 有没有注册奖励
});
}
/**
* @return void
*/
private function addUserThird(): void
{
$model = new UserThird();
$model->user_id = $this->userId;
$model->open_id = $this->openId;
$model->union_id = $this->unionId;
if (!$model->save()) throw new ApiException('注册失败-00001');
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* This service file is part of item.
*
* @author ctexthuang
* @contact ctexthuang@qq.com
*/
declare(strict_types=1);
namespace App\Service\ServiceTrait\Api;
use App\Exception\ApiException;
use App\Lib\Log;
use GuzzleHttp\Exception\GuzzleException;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Guzzle\ClientFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use function Hyperf\Config\config;
trait WxMiniTrait
{
/**
* 注入请求工厂类
* @var ClientFactory $clientFactory
*/
#[Inject]
protected ClientFactory $clientFactory;
/**
* 注入日志类
* @var Log
*/
#[Inject]
protected Log $log;
/**
* baseUri
* @var string
*/
protected string $BaseUri = 'https://api.weixin.qq.com';
/**
* @param string $code
* @return mixed
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function jsCodeGetOpenId(string $code): mixed
{
$url = sprintf(
'/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code',
config('system.wx_appid'),
config('system.wx_secret'),
$code
);
try {
$tencentResponse = $this->clientFactory->create([
'base_uri' => $this->BaseUri,
'timeout' => 5
])->get($url);
$contents = $tencentResponse->getBody()->getContents();
$this->log->info(__class__.':微信服务器返回token信息:', [$contents]);
$res = json_decode($contents,true);
if (empty($res['errcode']) || $res['errcode'] != 0) throw new ApiException($res['errmsg'] ?? '系统繁忙');
return $res;
}catch (GuzzleException $e) {
$this->log->debug(__CLASS__.':debug:'.$e->getMessage());
throw new ApiException($e->getMessage());
}
}
}

View File

@@ -13,6 +13,7 @@ return [
'handler' => [ 'handler' => [
'http' => [ 'http' => [
Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class,
App\Exception\Handler\ApiExceptionHandler::class,
App\Exception\Handler\AdminExceptionHandler::class, App\Exception\Handler\AdminExceptionHandler::class,
App\Exception\Handler\ValidationDataExceptionHandler::class, App\Exception\Handler\ValidationDataExceptionHandler::class,
App\Exception\Handler\AppExceptionHandler::class, App\Exception\Handler\AppExceptionHandler::class,

View File

@@ -20,5 +20,9 @@ return [
// admin jwt 过期时间 // admin jwt 过期时间
'admin_jwt_expire' => env('ADMIN_JWT_EXPIRE',86400 * 30), 'admin_jwt_expire' => env('ADMIN_JWT_EXPIRE',86400 * 30),
// admin 默认密码 // admin 默认密码
'admin_default_password' => '123456' 'admin_default_password' => '123456',
// 微信小程序的appid
'wx_appid' => env('WX_APPID',''),
// 微信小程序的secret
'wx_secret' => env('WX_SECRET',''),
]; ];