sqlite
This commit is contained in:
+3
-8
@@ -7,19 +7,14 @@ APP_URL=https://example.ru
|
||||
LOG_CHANNEL=stderr
|
||||
LOG_LEVEL=error
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=airservice
|
||||
DB_USERNAME=airservice
|
||||
DB_PASSWORD=
|
||||
DB_ROOT_PASSWORD=
|
||||
DB_CONNECTION=sqlite
|
||||
DB_DATABASE=/var/www/html/database/sqlite/database.sqlite
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
SESSION_DRIVER=database
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
# S3 (опционально, если нужно хранение файлов)
|
||||
|
||||
+3
-3
@@ -11,11 +11,11 @@ FROM php:8.2-fpm-alpine AS app
|
||||
WORKDIR /var/www/html
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev \
|
||||
zip unzip git mysql-client
|
||||
libpng-dev libjpeg-turbo-dev freetype-dev libzip-dev sqlite \
|
||||
zip unzip git
|
||||
|
||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install pdo pdo_mysql gd zip opcache pcntl
|
||||
&& docker-php-ext-install pdo pdo_sqlite gd zip opcache pcntl
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# AirService
|
||||
|
||||
Система управления заявками на техническое обслуживание (Service Desk). Сотрудники создают заявки, назначают приоритеты и отслеживают статус выполнения.
|
||||
Система управления заявками на техническое обслуживание (Service Desk).
|
||||
|
||||
**Стек:** Laravel 10, Livewire, Tailwind CSS, MySQL, Vite
|
||||
**Стек:** Laravel 10, Livewire, Tailwind CSS, SQLite, Vite
|
||||
|
||||
---
|
||||
|
||||
## Локальная разработка
|
||||
|
||||
**Требования:** PHP >= 8.2, Composer, Node.js >= 18, MySQL
|
||||
**Требования:** PHP >= 8.2, Composer, Node.js >= 18
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
@@ -18,9 +18,10 @@ composer install
|
||||
npm install
|
||||
|
||||
cp .env.example .env
|
||||
# Поменять APP_ENV=local, APP_DEBUG=true, APP_URL=http://localhost
|
||||
# Поменять DB_HOST=127.0.0.1, DB_USERNAME=root, убрать DB_ROOT_PASSWORD
|
||||
# Поменять APP_ENV=local, APP_DEBUG=true, APP_URL=http://localhost:8000
|
||||
php artisan key:generate
|
||||
|
||||
touch database/sqlite/database.sqlite
|
||||
php artisan migrate --seed
|
||||
```
|
||||
|
||||
@@ -34,13 +35,22 @@ php artisan serve
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Приложение: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
Приложение: http://127.0.0.1:8000
|
||||
|
||||
**Тестовые аккаунты** (пароль у всех: `password`):
|
||||
|
||||
| Email | Роль |
|
||||
|---|---|
|
||||
| admin@service.local | Администратор |
|
||||
| manager@service.local | Менеджер |
|
||||
| tech1@service.local | Техник |
|
||||
| client1@service.local | Клиент |
|
||||
|
||||
---
|
||||
|
||||
## Деплой на сервер (flamy / docker-runtime)
|
||||
## Деплой на сервер (Docker)
|
||||
|
||||
**Требования:** Docker, Docker Compose на `docker-runtime` (`10.20.0.20`).
|
||||
**Требования:** Docker, Docker Compose на сервере `10.20.0.20`.
|
||||
|
||||
### 1. Скопировать проект на сервер
|
||||
|
||||
@@ -49,7 +59,7 @@ ssh -i /root/.ssh/docker_vm_ed25519 crimson@10.20.0.20
|
||||
sudo mkdir -p /opt/docker-apps/airservice
|
||||
```
|
||||
|
||||
Скопировать файлы (с локальной машины):
|
||||
С локальной машины:
|
||||
|
||||
```bash
|
||||
rsync -avz --exclude='.git' --exclude='node_modules' --exclude='vendor' \
|
||||
@@ -67,27 +77,62 @@ nano .env
|
||||
Обязательно заполнить:
|
||||
|
||||
```env
|
||||
APP_KEY= # php artisan key:generate --show
|
||||
APP_KEY= # сгенерировать ниже
|
||||
APP_URL=https://your-domain.ru
|
||||
DB_PASSWORD= # придумать
|
||||
DB_ROOT_PASSWORD= # придумать
|
||||
```
|
||||
|
||||
### 3. Запустить
|
||||
Сгенерировать ключ:
|
||||
|
||||
```bash
|
||||
docker compose run --rm app php artisan key:generate --show
|
||||
# Скопировать результат в APP_KEY= в .env
|
||||
```
|
||||
|
||||
### 3. Собрать и запустить
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
docker compose logs -f --tail=100
|
||||
```
|
||||
|
||||
Проверить локально на сервере:
|
||||
### 4. Наполнить базу начальными данными (один раз)
|
||||
|
||||
```bash
|
||||
docker compose exec app php artisan db:seed
|
||||
```
|
||||
|
||||
После этого доступны тестовые аккаунты из таблицы выше.
|
||||
|
||||
### 5. Проверить
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs app --tail=50
|
||||
curl -I http://10.20.0.20:18201
|
||||
```
|
||||
|
||||
### 4. Открыть домен через Caddy (edge-proxy)
|
||||
### Пересборка после обновления кода
|
||||
|
||||
```bash
|
||||
rsync -avz --exclude='.git' --exclude='node_modules' --exclude='vendor' \
|
||||
./ crimson@10.20.0.20:/opt/docker-apps/airservice/
|
||||
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Миграции накатываются автоматически при старте контейнера. База данных (SQLite файл) хранится в Docker volume `airservice_sqlite_data` и не теряется при пересборке.
|
||||
|
||||
### Если нужно сбросить базу и накатить заново
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker volume rm airservice_sqlite_data
|
||||
docker compose up -d --build
|
||||
docker compose exec app php artisan db:seed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Открыть домен через Caddy (edge-proxy)
|
||||
|
||||
```bash
|
||||
ssh -i /root/.ssh/edge_vm_ed25519 crimson@192.168.0.80
|
||||
@@ -121,8 +166,7 @@ sudo systemctl reload caddy
|
||||
## Структура Docker-сервисов
|
||||
|
||||
| Сервис | Описание |
|
||||
|--------------------|-----------------------------------|
|
||||
|---|---|
|
||||
| `airservice_app` | PHP-FPM (Laravel) |
|
||||
| `airservice_nginx` | nginx, публикует порт `18201` |
|
||||
| `airservice_mysql` | MySQL 8.0, только внутри сети |
|
||||
| `airservice_queue` | Queue worker (`queue:work`) |
|
||||
| `airservice_nginx` | nginx, порт `18201` |
|
||||
| `airservice_queue` | Queue worker |
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -20,6 +21,13 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
if (config('database.default') === 'sqlite') {
|
||||
DB::statement('PRAGMA journal_mode=WAL');
|
||||
DB::statement('PRAGMA synchronous=NORMAL');
|
||||
DB::statement('PRAGMA busy_timeout=5000');
|
||||
DB::statement('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
try {
|
||||
$companyName = Setting::query()->where('key', 'company_name')->value('value');
|
||||
$timezone = Setting::query()->where('key', 'timezone')->value('value');
|
||||
|
||||
@@ -25,10 +25,13 @@ class RequestService
|
||||
{
|
||||
return DB::transaction(function () use ($data, $actor): ServiceRequest {
|
||||
$payload = $this->preparePayload($data);
|
||||
$payload['ticket_number'] = $this->generateTicketNumber();
|
||||
$payload['ticket_number'] = 'PENDING-' . uniqid('', true);
|
||||
|
||||
$request = ServiceRequest::query()->create($payload);
|
||||
|
||||
$request->ticket_number = sprintf('SRV-%04d', $request->id);
|
||||
$request->saveQuietly();
|
||||
|
||||
$this->persistAttachments($request, Arr::get($data, 'attachments', []), $actor);
|
||||
|
||||
event(new RequestCreated($request, $actor));
|
||||
@@ -111,11 +114,9 @@ class RequestService
|
||||
return $map[$request->status?->slug ?? StatusSlug::NEW->value] ?? [];
|
||||
}
|
||||
|
||||
public function generateTicketNumber(): string
|
||||
public function generateTicketNumber(int $id): string
|
||||
{
|
||||
$maxId = ServiceRequest::withTrashed()->max('id') ?? 0;
|
||||
|
||||
return sprintf('SRV-%04d', $maxId + 1);
|
||||
return sprintf('SRV-%04d', $id);
|
||||
}
|
||||
|
||||
private function preparePayload(array $data, ?ServiceRequest $request = null): array
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'mysql'),
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
+2
-2
@@ -85,7 +85,7 @@ return [
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'mysql'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
@@ -102,7 +102,7 @@ return [
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'mysql'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
|
||||
@@ -18,28 +18,75 @@ class ServiceRequestSeeder extends Seeder
|
||||
{
|
||||
$types = RequestType::query()->get();
|
||||
$priorities = Priority::query()->orderBy('level')->get();
|
||||
$statuses = Status::query()->get();
|
||||
$statuses = Status::query()->get()->keyBy('slug');
|
||||
$clients = User::query()->where('role', 'client')->get();
|
||||
$techs = User::query()->where('role', 'technician')->get();
|
||||
$equipment = Equipment::query()->get();
|
||||
|
||||
for ($i = 1; $i <= 25; $i++) {
|
||||
// Распределение: 10 активных, 8 в ожидании/в работе с дедлайном сегодня-завтра,
|
||||
// 4 решённых, 3 закрытых.
|
||||
$buckets = [
|
||||
// [slug, count, deadline_offset_hours от now]
|
||||
['new', 4, [+48, +168]], // новые — дедлайн через 2-7 дней
|
||||
['in_progress', 6, [+4, +48]], // в работе — дедлайн через 4-48 ч
|
||||
['waiting', 4, [+24, +96]], // ожидание — дедлайн через 1-4 дня
|
||||
['resolved', 5, [-72, -1]], // решены — дедлайн в прошлом
|
||||
['closed', 4, [-168, -24]], // закрыты — дедлайн в прошлом
|
||||
['cancelled', 2, [-240, -48]], // отменены
|
||||
];
|
||||
|
||||
$counter = 1;
|
||||
|
||||
foreach ($buckets as [$slug, $count, [$dlMin, $dlMax]]) {
|
||||
$status = $statuses[$slug] ?? $statuses->first();
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$type = $types->random();
|
||||
$priority = $priorities->random();
|
||||
$status = $statuses->random();
|
||||
$client = $clients->random();
|
||||
$assignee = $techs->random();
|
||||
$createdAt = now()->subDays(rand(0, 20))->subHours(rand(0, 23));
|
||||
$deadlineAt = (clone $createdAt)->addHours($type->sla_hours);
|
||||
|
||||
$deadlineAt = now()->addHours(rand($dlMin, $dlMax));
|
||||
|
||||
// created_at: задолго до дедлайна, но реалистично
|
||||
$createdAt = (clone $deadlineAt)->subHours($type->sla_hours + rand(0, 12));
|
||||
|
||||
// Не даём created_at быть в будущем
|
||||
if ($createdAt->isFuture()) {
|
||||
$createdAt = now()->subMinutes(rand(10, 120));
|
||||
}
|
||||
|
||||
$startedAt = null;
|
||||
$completedAt = null;
|
||||
$resolutionNotes = null;
|
||||
$clientRating = null;
|
||||
$clientComment = null;
|
||||
|
||||
if (in_array($slug, ['in_progress', 'resolved', 'closed'])) {
|
||||
$startedAt = (clone $createdAt)->addHours(rand(1, 4));
|
||||
}
|
||||
|
||||
if (in_array($slug, ['resolved', 'closed'])) {
|
||||
$completedAt = (clone $deadlineAt)->subHours(rand(0, 6));
|
||||
$resolutionNotes = fake()->sentence(12);
|
||||
}
|
||||
|
||||
if ($slug === 'closed') {
|
||||
$clientRating = rand(3, 5);
|
||||
$clientComment = fake()->sentence();
|
||||
}
|
||||
|
||||
$request = ServiceRequest::query()->create([
|
||||
'ticket_number' => sprintf('SRV-%04d', $i),
|
||||
'ticket_number' => sprintf('SRV-%04d', $counter),
|
||||
'title' => fake()->randomElement([
|
||||
'Не включается рабочая станция',
|
||||
'Проблема с печатью документов',
|
||||
'Нет доступа к корпоративной сети',
|
||||
'Требуется установка ПО',
|
||||
'Сильный шум вентилятора',
|
||||
'Зависает операционная система',
|
||||
'Не работает монитор',
|
||||
'Проблема с принтером',
|
||||
]),
|
||||
'description' => fake()->paragraph(3),
|
||||
'type_id' => $type->id,
|
||||
@@ -49,28 +96,26 @@ class ServiceRequestSeeder extends Seeder
|
||||
'assigned_to' => $assignee->id,
|
||||
'equipment_id' => rand(0, 100) > 20 ? $equipment->random()?->id : null,
|
||||
'location' => fake()->randomElement(['Офис 101', 'Офис 204', 'Склад', 'Переговорная 2']),
|
||||
'planned_at' => rand(0, 100) > 50 ? $createdAt->copy()->addHours(rand(2, 24)) : null,
|
||||
'planned_at' => rand(0, 100) > 50 ? (clone $createdAt)->addHours(rand(2, 24)) : null,
|
||||
'deadline_at' => $deadlineAt,
|
||||
'started_at' => rand(0, 100) > 40 ? $createdAt->copy()->addHours(rand(1, 8)) : null,
|
||||
'completed_at' => $status->is_final ? $deadlineAt->copy()->subHours(rand(0, 6)) : null,
|
||||
'resolution_notes' => $status->slug === 'resolved' || $status->slug === 'closed'
|
||||
? fake()->sentence(12)
|
||||
: null,
|
||||
'client_rating' => $status->slug === 'closed' ? rand(3, 5) : null,
|
||||
'client_comment' => $status->slug === 'closed' ? fake()->sentence() : null,
|
||||
'started_at' => $startedAt,
|
||||
'completed_at' => $completedAt,
|
||||
'resolution_notes' => $resolutionNotes,
|
||||
'client_rating' => $clientRating,
|
||||
'client_comment' => $clientComment,
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
for ($commentIndex = 1; $commentIndex <= rand(1, 3); $commentIndex++) {
|
||||
for ($c = 1; $c <= rand(1, 3); $c++) {
|
||||
$author = rand(0, 1) ? $client : $assignee;
|
||||
RequestComment::query()->create([
|
||||
'request_id' => $request->id,
|
||||
'user_id' => $author->id,
|
||||
'body' => fake()->sentence(rand(10, 20)),
|
||||
'is_internal' => $author->role !== 'client' && rand(0, 100) > 60,
|
||||
'created_at' => $createdAt->copy()->addHours($commentIndex),
|
||||
'updated_at' => $createdAt->copy()->addHours($commentIndex),
|
||||
'created_at' => (clone $createdAt)->addHours($c),
|
||||
'updated_at' => (clone $createdAt)->addHours($c),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,6 +128,9 @@ class ServiceRequestSeeder extends Seeder
|
||||
'ip_address' => '127.0.0.1',
|
||||
'created_at' => $createdAt,
|
||||
]);
|
||||
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-26
@@ -15,11 +15,9 @@ services:
|
||||
volumes:
|
||||
- storage_uploads:/var/www/html/storage/app/public
|
||||
- storage_logs:/var/www/html/storage/logs
|
||||
- sqlite_data:/var/www/html/database/sqlite
|
||||
networks:
|
||||
- backend
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "php-fpm -t 2>&1 | grep -q 'successful' && echo ok || exit 1"]
|
||||
interval: 30s
|
||||
@@ -40,34 +38,13 @@ services:
|
||||
volumes:
|
||||
- storage_uploads:/var/www/html/storage/app/public
|
||||
- storage_logs:/var/www/html/storage/logs
|
||||
- sqlite_data:/var/www/html/database/sqlite
|
||||
networks:
|
||||
- backend
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: airservice_mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_DATABASE: ${DB_DATABASE}
|
||||
MYSQL_USER: ${DB_USERNAME}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
networks:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "--password=${DB_ROOT_PASSWORD}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
nginx:
|
||||
build:
|
||||
context: .
|
||||
@@ -96,6 +73,6 @@ networks:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
sqlite_data:
|
||||
storage_uploads:
|
||||
storage_logs:
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ "$DB_CONNECTION" = "sqlite" ]; then
|
||||
DB_DIR=$(dirname "$DB_DATABASE")
|
||||
mkdir -p "$DB_DIR"
|
||||
if [ ! -f "$DB_DATABASE" ]; then
|
||||
touch "$DB_DATABASE"
|
||||
fi
|
||||
chown -R www-data:www-data "$DB_DIR"
|
||||
chmod 664 "$DB_DATABASE"
|
||||
fi
|
||||
|
||||
php artisan migrate --force
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
|
||||
Reference in New Issue
Block a user