站内消息和短信通知

main
Jing Li 2024-04-29 00:07:18 +08:00
parent 1203c5cb89
commit 257c29c49c
18 changed files with 569 additions and 95 deletions

View File

@ -2,22 +2,16 @@
namespace App\Admin\Services;
use App\Enums\MessageType;
use App\Enums\{CheckStatus, CheckType};
use App\Events\WorkflowCheckFailed;
use App\Events\WorkflowCheckNext;
use App\Events\WorkflowCheckSuccess;
use App\Models\Employee;
use App\Models\EmployeePromotion;
use App\Models\EmployeeSignRepair;
use App\Models\HolidayApply;
use App\Models\Keyword;
use App\Models\OfficalBusiness;
use App\Models\OvertimeApply;
use App\Models\Reimbursement;
use App\Models\Store;
use App\Models\Workflow;
use App\Models\WorkflowCheck;
use App\Models\WorkflowLog;
use App\Services\MessageService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
@ -48,6 +42,10 @@ class WorkFlowService extends BaseService
admin_abort('正在审核中');
}
$check->update([
'apply_at' => now(),
]);
$workflow = Workflow::where('key', $check->key)->first();
// 没有配置审核流程, 直接通过
if (! $workflow || ! $workflow->config) {
@ -110,23 +108,7 @@ class WorkFlowService extends BaseService
$check->subject->checkSuccess();
if ($employee = $check->employee) {
$text = $this->mapSubjectTypeText($check->subject_type);
if ($text !== '') {
(new MessageService())->create(
MessageType::Approval,
'审批提醒',
"您有一条【{$text}】已通过审核。",
[$employee],
[
'workflow_check' => [
'subject_id' => $check->subject_id,
'subject_type' => $check->subject_type,
],
],
);
}
}
WorkflowCheckSuccess::dispatch($check->withoutRelations());
return true;
}
@ -144,23 +126,7 @@ class WorkFlowService extends BaseService
$check->subject->checkFail();
if ($employee = $check->employee) {
$text = $this->mapSubjectTypeText($check->subject_type);
if ($text !== '') {
(new MessageService())->create(
MessageType::Approval,
'审批提醒',
"您有一条【{$text}】未通过审核。",
[$employee],
[
'workflow_check' => [
'subject_id' => $check->subject_id,
'subject_type' => $check->subject_type,
],
],
);
}
}
WorkflowCheckFailed::dispatch($check->withoutRelations());
return true;
}
@ -233,35 +199,7 @@ class WorkFlowService extends BaseService
]);
$log->update(['check_status' => CheckStatus::Processing]);
$employees = [];
switch ($log->check_type) {
case CheckType::Job:
[$storeId, $jobId] = explode('-', $log->check_value);
$employees = Employee::where('store_id', $storeId)
->whereHas('jobs', fn ($query) => $query->where('job_id', $jobId))
->pluck('id')
->all();
break;
case CheckType::User:
$employees = [$log->check_value];
break;
}
if (count($employees)) {
$text = $this->mapSubjectTypeText($check->subject_type);
if ($text !== '') {
(new MessageService())->create(
MessageType::Approval,
'审批提醒',
"您有一条【{$text}】待审批。",
$employees,
[
'workflow_log' => ['id' => $log->id],
],
);
}
}
WorkflowCheckNext::dispatch($log);
// 自动审核通过
if ($this->authCheck($check->employee, $log)) {
@ -343,17 +281,4 @@ class WorkFlowService extends BaseService
return true;
}
protected function mapSubjectTypeText(string $subjectType): string
{
return match (Relation::getMorphedModel($subjectType)) {
EmployeeSignRepair::class => '补卡申请',
HolidayApply::class => '请假申请',
OvertimeApply::class => '加班申请',
OfficalBusiness::class => '出差报备',
EmployeePromotion::class => '升职申请',
Reimbursement::class => '报销申请',
default => '',
};
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Events;
use App\Models\WorkflowCheck;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class WorkflowCheckFailed implements ShouldDispatchAfterCommit
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public WorkflowCheck $workflowCheck
) {}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Events;
use App\Models\WorkflowLog;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class WorkflowCheckNext implements ShouldDispatchAfterCommit
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public WorkflowLog $workflowLog,
) {}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Events;
use App\Models\WorkflowCheck;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class WorkflowCheckSuccess implements ShouldDispatchAfterCommit
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public WorkflowCheck $workflowCheck
) {}
}

View File

@ -2,13 +2,14 @@
namespace App\Exceptions;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Illuminate\Http\{Request, Response};
use Illuminate\Validation\ValidationException;
use Overtrue\EasySms\Exceptions\NoGatewayAvailableException;
use Slowlyo\OwlAdmin\Exceptions\AdminException;
use Illuminate\Auth\AuthenticationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class Handler extends ExceptionHandler
{
@ -40,6 +41,13 @@ class Handler extends ExceptionHandler
$this->renderable(function (NotFoundHttpException $e, Request $request) {
return response(['code' => Response::HTTP_NOT_FOUND, 'message' => $e->getMessage()], Response::HTTP_NOT_FOUND);
});
$this->reportable(function (NoGatewayAvailableException $e) {
foreach ($e->getExceptions() as $exception) {
$this->report($exception);
}
return false;
});
}
protected function invalidJson($request, ValidationException $exception)

View File

@ -0,0 +1,44 @@
<?php
namespace App\Listeners;
use App\Enums\MessageType;
use App\Events\WorkflowCheckFailed;
use App\Services\MessageService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class CreateWorkflowCheckFailedMessage implements ShouldQueue
{
use InteractsWithQueue;
/**
* Create the event listener.
*/
public function __construct(
public MessageService $messageService,
) {}
/**
* Handle the event.
*/
public function handle(WorkflowCheckFailed $event): void
{
$subjectTypeText = $event->workflowCheck->subjectTypeText();
if ($subjectTypeText === '') {
return;
}
if (is_null($event->workflowCheck->employee)) {
return;
}
$this->messageService->create(
MessageType::Approval,
'审批提醒',
"您有一条【{$subjectTypeText}】未通过审核。",
[$event->workflowCheck->employee],
['workflow_check' => $event->workflowCheck],
);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Listeners;
use App\Enums\CheckType;
use App\Enums\MessageType;
use App\Events\WorkflowCheckNext;
use App\Models\Employee;
use App\Services\MessageService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class CreateWorkflowCheckNextMessage implements ShouldQueue
{
use InteractsWithQueue;
/**
* Create the event listener.
*/
public function __construct(
public MessageService $messageService,
) {}
/**
* Handle the event.
*/
public function handle(WorkflowCheckNext $event): void
{
$subjectTypeText = $event->workflowLog->check->subjectTypeText();
if ($subjectTypeText === '') {
return;
}
$employees = collect();
switch ($event->workflowLog->check_type) {
case CheckType::Job:
[$storeId, $jobId] = explode('-', $event->workflowLog->check_value);
$employees = Employee::where('store_id', $storeId)
->whereHas('jobs', fn ($query) => $query->where('job_id', $jobId))
->pluck('id');
break;
case CheckType::User:
$employees->push($event->workflowLog->check_value);
break;
}
if ($employees->isEmpty()) {
return;
}
$this->messageService->create(
MessageType::Approval,
'审批提醒',
"您有一条【{$subjectTypeText}】待审批。",
$employees->all(),
['workflow_log' => $event->workflowLog],
);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Listeners;
use App\Enums\MessageType;
use App\Events\WorkflowCheckSuccess;
use App\Services\MessageService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class CreateWorkflowCheckSuccessMessage implements ShouldQueue
{
use InteractsWithQueue;
/**
* Create the event listener.
*/
public function __construct(
public MessageService $messageService,
) {}
/**
* Handle the event.
*/
public function handle(WorkflowCheckSuccess $event): void
{
$subjectTypeText = $event->workflowCheck->subjectTypeText();
if ($subjectTypeText === '') {
return;
}
if (is_null($event->workflowCheck->employee)) {
return;
}
$this->messageService->create(
MessageType::Approval,
'审批提醒',
"您有一条【{$subjectTypeText}】已通过审核。",
[$event->workflowCheck->employee],
['workflow_check' => $event->workflowCheck],
);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Listeners;
use App\Events\WorkflowCheckFailed;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Overtrue\EasySms\EasySms;
class SendWorkflowCheckFailedNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(WorkflowCheckFailed $event): void
{
$subjectTypeText = $event->workflowCheck->subjectTypeText();
if ($subjectTypeText === '') {
return;
}
if (empty($phone = $event->workflowCheck->employee?->phone)) {
return;
}
if (is_null($applyAt = $event->workflowCheck->apply_at)) {
$applyAt = $event->workflowCheck->created_at;
}
$client = new EasySms(config('easysms'));
$client->send($phone, [
'content' => function($gateway) use ($applyAt, $subjectTypeText) {
return "您于{$applyAt->toDateString()}提交的{$subjectTypeText}未通过审批,请登录查看。";
},
'template' => function($gateway) {
if ($gateway->getName() == 'aliyun') {
return 'SMS_465890235';
}
},
'data' => function($gateway) use ($applyAt, $subjectTypeText) {
return [
'time' => $applyAt->toDateString(),
'work_msg' => $subjectTypeText,
];
},
]);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Listeners;
use App\Enums\CheckType;
use App\Events\WorkflowCheckNext;
use App\Models\Employee;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Overtrue\EasySms\EasySms;
use Throwable;
class SendWorkflowCheckNextNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(WorkflowCheckNext $event): void
{
$subjectTypeText = $event->workflowLog->check->subjectTypeText();
if ($subjectTypeText === '') {
return;
}
$employees = collect();
switch ($event->workflowLog->check_type) {
case CheckType::Job:
[$storeId, $jobId] = explode('-', $event->workflowLog->check_value);
$employees = Employee::where('store_id', $storeId)
->whereHas('jobs', fn ($query) => $query->where('job_id', $jobId))
->get();
break;
case CheckType::User:
if ($employee = Employee::find($event->workflowLog->check_value)) {
$employees->push($employee);
}
break;
}
$client = new EasySms(config('easysms'));
foreach ($employees as $employee) {
try {
$client->send($employee->phone, [
'content' => function($gateway) use ($subjectTypeText) {
return "您有一条{$subjectTypeText}待处理,请尽快登录处理。";
},
'template' => function($gateway) {
if ($gateway->getName() == 'aliyun') {
return 'SMS_465885234';
}
},
'data' => function($gateway) use ($subjectTypeText) {
return [
'work_msg' => $subjectTypeText,
];
},
]);
} catch (Throwable $e) {
report($e);
}
}
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Listeners;
use App\Events\WorkflowCheckSuccess;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Overtrue\EasySms\EasySms;
class SendWorkflowCheckSuccessNotification implements ShouldQueue
{
use InteractsWithQueue;
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(WorkflowCheckSuccess $event): void
{
$subjectTypeText = $event->workflowCheck->subjectTypeText();
if ($subjectTypeText === '') {
return;
}
if (empty($phone = $event->workflowCheck->employee?->phone)) {
return;
}
if (is_null($applyAt = $event->workflowCheck->apply_at)) {
$applyAt = $event->workflowCheck->created_at;
}
$client = new EasySms(config('easysms'));
$client->send($phone, [
'content' => function($gateway) use ($applyAt, $subjectTypeText) {
return "您于{$applyAt->toDateString()}提交的{$subjectTypeText}已通过审批,请登录查看。";
},
'template' => function($gateway) {
if ($gateway->getName() == 'aliyun') {
return 'SMS_465915258';
}
},
'data' => function($gateway) use ($applyAt, $subjectTypeText) {
return [
'time' => $applyAt->toDateString(),
'work_msg' => $subjectTypeText,
];
},
]);
}
}

View File

@ -6,6 +6,7 @@ use App\Enums\{CheckStatus, CheckType};
use App\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
/**
* 审核申请
@ -18,10 +19,11 @@ class WorkflowCheck extends Model
'check_status' => CheckStatus::None,
];
protected $fillable = ['subject_type', 'subject_id', 'subject_data', 'key', 'employee_id', 'check_status', 'checked_at', 'check_remarks', 'check_type', 'check_value', 'check_name'];
protected $fillable = ['subject_type', 'subject_id', 'subject_data', 'key', 'employee_id', 'check_status', 'checked_at', 'check_remarks', 'check_type', 'check_value', 'check_name', 'apply_at'];
protected $casts = [
'subject_data' => 'json',
'apply_at' => 'datetime',
'check_status' => CheckStatus::class,
'checked_at' => 'datetime',
'check_type' => CheckType::class,
@ -64,4 +66,17 @@ class WorkflowCheck extends Model
{
return $this->check_status === CheckStatus::Fail;
}
public function subjectTypeText(): string
{
return match (Relation::getMorphedModel($this->subject_type)) {
EmployeeSignRepair::class => '补卡申请',
HolidayApply::class => '请假申请',
OvertimeApply::class => '加班申请',
OfficalBusiness::class => '出差报备',
EmployeePromotion::class => '升职申请',
Reimbursement::class => '报销申请',
default => '',
};
}
}

View File

@ -5,7 +5,6 @@ namespace App\Models;
use App\Enums\CheckStatus;
use App\Enums\CheckType;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasDateTimeFormatter;
/**
* 审核流水
@ -46,4 +45,9 @@ class WorkflowLog extends Model
}
return $builder->whereIn('check_value', $checkValue);
}
public function isCheckProcessing(): bool
{
return $this->check_status === CheckStatus::Processing;
}
}

View File

@ -2,8 +2,6 @@
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
@ -15,8 +13,17 @@ class EventServiceProvider extends ServiceProvider
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
\App\Events\WorkflowCheckNext::class => [
\App\Listeners\CreateWorkflowCheckNextMessage::class,
\App\Listeners\SendWorkflowCheckNextNotification::class,
],
\App\Events\WorkflowCheckSuccess::class => [
\App\Listeners\CreateWorkflowCheckSuccessMessage::class,
\App\Listeners\SendWorkflowCheckSuccessNotification::class,
],
\App\Events\WorkflowCheckFailed::class => [
\App\Listeners\CreateWorkflowCheckFailedMessage::class,
\App\Listeners\SendWorkflowCheckFailedNotification::class,
],
];

View File

@ -11,6 +11,7 @@
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8",
"overtrue/easy-sms": "^3.0",
"rap2hpoutre/fast-excel": "^5.4",
"slowlyo/owl-admin": "^3.5",
"tucker-eric/eloquentfilter": "^3.2"

58
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6ce90738fadc40a447dfead6b131ec62",
"content-hash": "e50204833e31b832f0bb8b3a5438e5ab",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@ -2087,6 +2087,60 @@
],
"time": "2024-01-09T09:30:37+00:00"
},
{
"name": "overtrue/easy-sms",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/overtrue/easy-sms.git",
"reference": "fa43b42ab936f5fbd6684f6469d16f0ab409c0d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/overtrue/easy-sms/zipball/fa43b42ab936f5fbd6684f6469d16f0ab409c0d8",
"reference": "fa43b42ab936f5fbd6684f6469d16f0ab409c0d8",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": "^6.2 || ^7.0",
"php": ">=8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.54",
"jetbrains/phpstorm-attributes": "^1.0",
"mockery/mockery": "^1.4.2",
"phpunit/phpunit": "^9.5.8"
},
"type": "library",
"autoload": {
"psr-4": {
"Overtrue\\EasySms\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "overtrue",
"email": "i@overtrue.me"
}
],
"description": "The easiest way to send short message.",
"support": {
"issues": "https://github.com/overtrue/easy-sms/issues",
"source": "https://github.com/overtrue/easy-sms/tree/3.0.0"
},
"funding": [
{
"url": "https://github.com/overtrue",
"type": "github"
}
],
"time": "2024-04-18T08:21:47+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.2",
@ -6634,5 +6688,5 @@
"php": "^8.1"
},
"platform-dev": [],
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.3.0"
}

29
config/easysms.php 100644
View File

@ -0,0 +1,29 @@
<?php
return [
// HTTP 请求的超时时间(秒)
'timeout' => 5.0,
// 默认发送配置
'default' => [
// 网关调用策略,默认:顺序调用
'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,
// 默认可用的发送网关
'gateways' => [
'aliyun',
],
],
// 可用的网关配置
'gateways' => [
'errorlog' => [
'file' => storage_path('logs/easysms.log'),
],
'aliyun' => [
'access_key_id' => env('ALIYUN_SMS_ACCESS_KEY_ID'),
'access_key_secret' => env('ALIYUN_SMS_ACCESS_KEY_SECRET'),
'sign_name' => env('ALIYUN_SMS_SIGN_NAME'),
],
],
];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('workflow_checks', function (Blueprint $table) {
$table->timestamp('apply_at')->nullable()->comment('审批申请时间');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('workflow_checks', function (Blueprint $table) {
$table->dropColumn(['apply_at']);
});
}
};