提交
This commit is contained in:
126
app/service/AwsUploadService.php
Normal file
126
app/service/AwsUploadService.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
namespace app\service;
|
||||
use Aws\S3\S3Client;
|
||||
use think\facade\Log;
|
||||
use think\File;
|
||||
|
||||
class AwsUploadService
|
||||
{
|
||||
/**
|
||||
* 处理表单文件上传并转发到目标服务器
|
||||
*
|
||||
* @param string $fileField 表单文件字段名
|
||||
* @param array $extraFields 额外传递的字段
|
||||
* @return array 包含上传结果的数组
|
||||
*/
|
||||
public function uploadAndForward( File $file, string $fileField,array $extraFields = []): array
|
||||
{
|
||||
|
||||
$mimeType = $file->getMime();
|
||||
$s3config = config('filesystem.disks.s3');
|
||||
$s3 = new S3Client($s3config);
|
||||
try {
|
||||
$config = config('app.upload_conf');
|
||||
$configs=[];
|
||||
foreach ($config as $key=>$con){
|
||||
$configs[] = $key.':'.$con;
|
||||
}
|
||||
|
||||
// 验证文件
|
||||
$validate = validate([
|
||||
$fileField =>implode('|',$configs),
|
||||
]);
|
||||
|
||||
if (!$validate->check([$fileField => $file])) {
|
||||
return $this->createErrorResult($validate->getError());
|
||||
}
|
||||
|
||||
// 根据文件类型设置 Content-Type
|
||||
if (in_array($mimeType, config('app.imageMimeTypes'))) {
|
||||
$contentType = $mimeType; // 使用图片的实际 MIME 类型
|
||||
} else {
|
||||
$contentType = 'application/octet-stream'; // 非图片文件使用默认类型
|
||||
}
|
||||
$checkBucket = $this->checkBucket($s3, $extraFields['Bucket']);
|
||||
if (!$checkBucket) {
|
||||
return $this->createErrorResult('Bucket error');
|
||||
}
|
||||
$s3->putObject([
|
||||
'Bucket' => $extraFields['Bucket'],
|
||||
'Key' => $extraFields['Key'],
|
||||
'Body' => fopen($file->getRealPath(), 'r'),
|
||||
'ACL' => 'public-read',
|
||||
'ContentType' => $contentType, // 设置检测到的 Content-Type
|
||||
]);
|
||||
return [
|
||||
'code' => 1,
|
||||
'data' =>[
|
||||
'savename'=>$s3config['endpoint'].'/'.$extraFields['Bucket'].'/'.$extraFields['Key']
|
||||
] ,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return $this->createErrorResult('上传过程中发生异常: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
public function checkBucket($s3, $bucket)
|
||||
{
|
||||
try {
|
||||
$buckets = $s3->listBuckets()->get('Buckets');
|
||||
$buckets_arr = array_column($buckets, 'Name');
|
||||
if (!in_array($bucket, $buckets_arr)) {
|
||||
$s3->createBucket([
|
||||
'Bucket' => $bucket,
|
||||
]);
|
||||
$policy = [
|
||||
'Version' => '2012-10-17',
|
||||
'Statement' => [
|
||||
[
|
||||
'Effect' => 'Allow',
|
||||
'Principal' => ['AWS' => ['*']],
|
||||
'Action' => [
|
||||
's3:GetBucketLocation',
|
||||
's3:ListBucket',
|
||||
's3:ListBucketMultipartUploads',
|
||||
],
|
||||
'Resource' => "arn:aws:s3:::$bucket",
|
||||
],
|
||||
[
|
||||
'Effect' => 'Allow',
|
||||
'Principal' => ['AWS' => ['*']],
|
||||
'Action' => [
|
||||
's3:DeleteObject',
|
||||
's3:GetObject',
|
||||
's3:ListMultipartUploadParts',
|
||||
's3:PutObject',
|
||||
's3:AbortMultipartUpload',
|
||||
],
|
||||
'Resource' => "arn:aws:s3:::$bucket/*",
|
||||
],
|
||||
],
|
||||
];
|
||||
$s3->putBucketPolicy([
|
||||
'Bucket' => $bucket,
|
||||
'Policy' => json_encode($policy),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::info('checkBucket error:' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* 创建错误结果数组
|
||||
*
|
||||
* @param string $message 错误信息
|
||||
* @return array 错误结果
|
||||
*/
|
||||
private function createErrorResult(string $message): array
|
||||
{
|
||||
return [
|
||||
'code' => 0,
|
||||
'error' => $message
|
||||
];
|
||||
}
|
||||
}
|
||||
189
app/service/BaiduOcrService.php
Normal file
189
app/service/BaiduOcrService.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use Curl\Curl;
|
||||
|
||||
class BaiduOcrService
|
||||
{
|
||||
private $access_token;
|
||||
private $access_token_url = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials';
|
||||
|
||||
private $ocr_url='https://aip.baidubce.com/rest/2.0/ocr/v1/';
|
||||
public function __construct()
|
||||
{
|
||||
$this->getAccessToken();
|
||||
}
|
||||
/**
|
||||
*
|
||||
*护照识别
|
||||
* @param string $fileData 图片的二进制数据
|
||||
* @return array
|
||||
*/
|
||||
function passport($fileData)
|
||||
{
|
||||
if ($fileData === false) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '无法读取上传的文件',
|
||||
];
|
||||
}
|
||||
if (empty($this->access_token)){
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '无法识别',
|
||||
];
|
||||
}
|
||||
$curl = new Curl();
|
||||
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, false);
|
||||
$curl->setOpt(CURLOPT_SSL_VERIFYHOST, 0);
|
||||
$curl->post($this->ocr_url.'passport'.'?access_token='.$this->access_token, [
|
||||
'image' => base64_encode($fileData),
|
||||
]);
|
||||
if ($curl->error) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => $curl->errorMessage,
|
||||
];
|
||||
} else {
|
||||
if (isset($curl->response->error_code)){
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '解析失败',
|
||||
];
|
||||
}
|
||||
$datas = json_decode(json_encode($curl->response), true);
|
||||
return [
|
||||
'code' => 1,
|
||||
'data' => parsePassport($datas)
|
||||
];
|
||||
}
|
||||
}
|
||||
public function getAccessToken()
|
||||
{
|
||||
if (file_exists(BAIDUACCESSTOKENDIR.'baidu_access_token')){
|
||||
$access_token = file_get_contents(BAIDUACCESSTOKENDIR.'baidu_access_token');
|
||||
$access_token = json_decode($access_token,true);
|
||||
if ($access_token['expires_time'] > time()){
|
||||
$this->access_token = $access_token['access_token'];
|
||||
return [
|
||||
'code' => 1,
|
||||
'msg' => "获取成功",
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
$this->access_token_url = $this->access_token_url.'&client_id='.config('app.baidu_ocr.appkey').'&client_secret='.config('app.baidu_ocr.appsecket');
|
||||
$curl = new Curl();
|
||||
$curl->setHeader('Content-Type', 'application/json');
|
||||
$curl->post($this->access_token_url);
|
||||
if ($curl->error) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => $curl->errorMessage,
|
||||
];
|
||||
}
|
||||
//进行存储access_token
|
||||
$access_token = $curl->response->access_token;
|
||||
$this->access_token = $access_token;
|
||||
file_put_contents(BAIDUACCESSTOKENDIR.'baidu_access_token',json_encode(
|
||||
[
|
||||
'access_token' => $access_token,
|
||||
'expires_time' => time() + $curl->response->expires_in - 600,
|
||||
]
|
||||
));
|
||||
return [
|
||||
'code' => 1,
|
||||
'msg' => "获取成功",
|
||||
];
|
||||
}
|
||||
/**
|
||||
*
|
||||
*身份证识别
|
||||
* @param string $fileData 图片的二进制数据
|
||||
* @return array
|
||||
*/
|
||||
function identification($fileData)
|
||||
{
|
||||
if ($fileData === false) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '无法读取上传的文件',
|
||||
];
|
||||
}
|
||||
if (empty($this->access_token)){
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '无法识别',
|
||||
];
|
||||
}
|
||||
$curl = new Curl();
|
||||
$curl->post($this->ocr_url.'idcard'.'?access_token='.$this->access_token, [
|
||||
'id_card_side' => "front",
|
||||
'image' => base64_encode($fileData),
|
||||
]);
|
||||
if ($curl->error) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => $curl->errorMessage,
|
||||
];
|
||||
} else {
|
||||
if (isset($curl->response->error_code)){
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '解析失败',
|
||||
];
|
||||
}
|
||||
$datas = json_decode(json_encode($curl->response), true);
|
||||
return [
|
||||
'code' => 1,
|
||||
'data' => parseIdentification($datas)
|
||||
];
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
*护照识别
|
||||
* @param string $fileData 图片的二进制数据
|
||||
* @return array 包含压缩后图片信息的数组
|
||||
*/
|
||||
function visa($fileData)
|
||||
{
|
||||
if ($fileData === false) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '无法读取上传的文件',
|
||||
];
|
||||
}
|
||||
if (empty($this->access_token)){
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '无法识别',
|
||||
];
|
||||
}
|
||||
$curl = new Curl();
|
||||
$curl->post($this->ocr_url.'accurate_basic'.'?access_token='.$this->access_token, [
|
||||
'image' => base64_encode($fileData),
|
||||
'language_type'=>'ENG'
|
||||
]);
|
||||
if ($curl->error) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => $curl->errorMessage,
|
||||
];
|
||||
} else {
|
||||
if (isset($curl->response->error_code)){
|
||||
return [
|
||||
'code' => 0,
|
||||
'msg' => '解析失败',
|
||||
];
|
||||
}
|
||||
$datas = json_decode(json_encode($curl->response), true);
|
||||
return [
|
||||
'code' => 1,
|
||||
'data' => parseVisa($datas)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
186
app/service/CompressImgService.php
Normal file
186
app/service/CompressImgService.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
/**
|
||||
* 压缩二进制流图片
|
||||
*/
|
||||
class CompressImgService
|
||||
{
|
||||
/**
|
||||
* 压缩通过表单上传的图片
|
||||
*
|
||||
* @param string $fileData 图片的二进制数据
|
||||
* @param int $quality 压缩质量(0-100,仅适用于JPEG和WebP)
|
||||
* @param string|null $targetFormat 目标格式('jpeg', 'png', 'webp',默认使用原格式)
|
||||
* @return array 包含压缩后图片信息的数组
|
||||
*/
|
||||
function compress($fileData, $quality = 80, $targetFormat = null)
|
||||
{
|
||||
|
||||
// 读取上传的二进制数据
|
||||
$imageData = file_get_contents($fileData);
|
||||
if ($imageData === false) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '无法读取上传的文件',
|
||||
];
|
||||
}
|
||||
|
||||
// 获取图片信息
|
||||
$imageInfo = getimagesizefromstring($imageData);
|
||||
if (!$imageInfo) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '无法解析图片信息',
|
||||
];
|
||||
}
|
||||
|
||||
// 确定原始和目标格式
|
||||
$originalMimeType = $imageInfo['mime'];
|
||||
$originalExtension = image_type_to_extension($imageInfo[2], false);
|
||||
|
||||
if ($targetFormat === null) {
|
||||
$targetFormat = $originalExtension;
|
||||
}
|
||||
|
||||
// 创建图片资源
|
||||
switch (strtolower($originalExtension)) {
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
$image = imagecreatefromstring($imageData);
|
||||
break;
|
||||
case 'png':
|
||||
$image = imagecreatefromstring($imageData);
|
||||
break;
|
||||
case 'webp':
|
||||
$image = imagecreatefromstring($imageData);
|
||||
break;
|
||||
default:
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '不支持的图片格式',
|
||||
];
|
||||
}
|
||||
|
||||
if (!$image) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => '无法创建图片资源',
|
||||
];
|
||||
}
|
||||
|
||||
// 压缩并转换图片
|
||||
ob_start();
|
||||
switch (strtolower($targetFormat)) {
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
imagejpeg($image, null, $quality);
|
||||
$compressedMimeType = 'image/jpeg';
|
||||
break;
|
||||
case 'png':
|
||||
$pngQuality = 9 - floor($quality / 11);
|
||||
imagepng($image, null, $pngQuality);
|
||||
$compressedMimeType = 'image/png';
|
||||
break;
|
||||
case 'webp':
|
||||
if (function_exists('imagewebp')) {
|
||||
imagewebp($image, null, $quality);
|
||||
$compressedMimeType = 'image/webp';
|
||||
} else {
|
||||
imagejpeg($image, null, $quality);
|
||||
$compressedMimeType = 'image/jpeg';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
imagedestroy($image);
|
||||
ob_end_clean();
|
||||
return [
|
||||
'code' => 0,
|
||||
'message' => "不支持的目标格式: {$targetFormat}",
|
||||
];
|
||||
}
|
||||
|
||||
$compressedData = ob_get_clean();
|
||||
imagedestroy($image);
|
||||
// 返回结果
|
||||
return [
|
||||
'code' => 1,
|
||||
'compressed_data' => $compressedData,
|
||||
'base64' => 'data:' . $compressedMimeType . ';base64,' . base64_encode($compressedData),
|
||||
];
|
||||
}
|
||||
|
||||
public function compressImage($filePath, $fileSize)
|
||||
{
|
||||
// 获取图片信息
|
||||
$imageInfo = getimagesize($filePath);
|
||||
if (!$imageInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mime = $imageInfo['mime'];
|
||||
|
||||
// 根据MIME类型创建图像资源
|
||||
switch ($mime) {
|
||||
case 'image/jpeg':
|
||||
$image = imagecreatefromjpeg($filePath);
|
||||
break;
|
||||
case 'image/png':
|
||||
$image = imagecreatefrompng($filePath);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$image = imagecreatefromgif($filePath);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$image) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算压缩质量(根据文件大小动态调整)
|
||||
$originalQuality = 85; // 初始质量
|
||||
$targetSize = 2.5 * 1024 * 1024; // 目标大小2.5MB
|
||||
|
||||
// 如果文件很大,降低初始质量
|
||||
if ($fileSize > 5 * 1024 * 1024) {
|
||||
$quality = 75;
|
||||
} elseif ($fileSize > 10 * 1024 * 1024) {
|
||||
$quality = 65;
|
||||
} else {
|
||||
$quality = $originalQuality;
|
||||
}
|
||||
|
||||
// 创建临时文件
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'compressed_');
|
||||
|
||||
// 保存压缩后的图片
|
||||
switch ($mime) {
|
||||
case 'image/jpeg':
|
||||
imagejpeg($image, $tempFile, $quality);
|
||||
break;
|
||||
case 'image/png':
|
||||
// PNG使用压缩级别(0-9),需要转换
|
||||
$pngQuality = 9 - round(($quality / 100) * 9);
|
||||
imagepng($image, $tempFile, $pngQuality);
|
||||
break;
|
||||
case 'image/gif':
|
||||
imagegif($image, $tempFile);
|
||||
break;
|
||||
}
|
||||
|
||||
// 释放内存
|
||||
imagedestroy($image);
|
||||
|
||||
// 检查压缩后文件大小,如果仍然大于3MB,继续压缩
|
||||
$compressedSize = filesize($tempFile);
|
||||
if ($compressedSize > 3 * 1024 * 1024) {
|
||||
// 递归压缩直到满足要求
|
||||
return $this->compressImage($tempFile, $compressedSize);
|
||||
}
|
||||
|
||||
return $tempFile;
|
||||
}
|
||||
|
||||
}
|
||||
45
app/service/SwooleService.php
Normal file
45
app/service/SwooleService.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace app\service;
|
||||
|
||||
use Swoole\Client;
|
||||
|
||||
class SwooleService
|
||||
{
|
||||
private static ?self $instance = null;
|
||||
private Client $client;
|
||||
private string $host = 'ossup.jzvisa.com';
|
||||
private int $port = 19501;
|
||||
private float $timeout = 0.5;
|
||||
|
||||
// 私有化构造函数
|
||||
private function __construct()
|
||||
{
|
||||
$this->client = new Client(SWOOLE_SOCK_TCP);
|
||||
if (!$this->client->connect($this->host, $this->port, $this->timeout)) {
|
||||
throw new \RuntimeException("Cannot connect to Swoole Server {$this->host}:{$this->port}");
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单例
|
||||
public static function getInstance(): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
// 发送任务
|
||||
public function sendTask(array $task): bool
|
||||
{
|
||||
$data = json_encode($task) . "\r\n";
|
||||
return $this->client->send($data);
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
public function close(): void
|
||||
{
|
||||
$this->client->close();
|
||||
self::$instance = null;
|
||||
}
|
||||
}
|
||||
133
app/service/UploadService.php
Normal file
133
app/service/UploadService.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
namespace app\service;
|
||||
use think\Exception;
|
||||
|
||||
class UploadService
|
||||
{
|
||||
/**
|
||||
* 处理表单文件上传并转发到目标服务器
|
||||
*
|
||||
* @param string $fileField 表单文件字段名
|
||||
* @param string $targetUrl 目标服务器URL
|
||||
* @param array $extraFields 额外传递的字段
|
||||
* @param array $headers 额外的请求头
|
||||
* @return array 包含上传结果的数组
|
||||
*/
|
||||
public function uploadAndForward(string $fileField, string $targetUrl, array $extraFields = [], array $headers = []): array
|
||||
{
|
||||
// 获取上传的文件
|
||||
$file = request()->file($fileField);
|
||||
|
||||
if (!$file) {
|
||||
return $this->createErrorResult('未上传文件或上传失败');
|
||||
}
|
||||
try {
|
||||
$config = config('app.upload_conf');
|
||||
$configs=[];
|
||||
foreach ($config as $key=>$con){
|
||||
$configs[] = $key.':'.$con;
|
||||
}
|
||||
|
||||
// 验证文件
|
||||
$validate = validate([
|
||||
$fileField =>implode('|',$configs),
|
||||
]);
|
||||
|
||||
if (!$validate->check([$fileField => $file])) {
|
||||
return $this->createErrorResult($validate->getError());
|
||||
}
|
||||
|
||||
// 使用cURL转发到目标服务器
|
||||
$result = $this->forwardToTargetServer($file, $targetUrl, $extraFields, $headers);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
return $this->createErrorResult('上传过程中发生异常: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用cURL将文件转发到目标服务器
|
||||
*
|
||||
* @param \think\File $file 文件对象
|
||||
* @param string $targetUrl 目标URL
|
||||
* @param array $extraFields 额外字段
|
||||
* @param array $headers 请求头
|
||||
* @return array 包含响应信息的数组
|
||||
*/
|
||||
private function forwardToTargetServer(\think\File $file, string $targetUrl, array $extraFields = [], array $headers = []): array
|
||||
{
|
||||
$ch = curl_init();
|
||||
|
||||
try {
|
||||
// 设置基本选项
|
||||
curl_setopt($ch, CURLOPT_URL, $targetUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 5分钟超时
|
||||
|
||||
// 构建上传数据
|
||||
$postData = [
|
||||
'file' => new \CURLFile(
|
||||
$file->getPathname(),
|
||||
$file->getOriginalMime(),
|
||||
$file->getOriginalName()
|
||||
)
|
||||
];
|
||||
|
||||
// 添加额外字段
|
||||
$postData = array_merge($postData, $extraFields);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||
|
||||
// 设置请求头
|
||||
$httpHeaders = [];
|
||||
foreach ($headers as $headerName => $headerValue) {
|
||||
$httpHeaders[] = "$headerName: $headerValue";
|
||||
}
|
||||
|
||||
if (!empty($httpHeaders)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlInfo = curl_getinfo($ch);
|
||||
|
||||
// 检查错误
|
||||
if ($response === false) {
|
||||
throw new Exception('cURL错误: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 1,
|
||||
'http_code' => $httpCode,
|
||||
'response' => $response,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'code' => 0,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
} finally {
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误结果数组
|
||||
*
|
||||
* @param string $message 错误信息
|
||||
* @return array 错误结果
|
||||
*/
|
||||
private function createErrorResult(string $message): array
|
||||
{
|
||||
return [
|
||||
'code' => 0,
|
||||
'error' => $message
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user