This commit is contained in:
antv
2026-07-14 10:46:34 +07:00
parent 47a8892623
commit 9370e2fb3c
5 changed files with 159 additions and 29 deletions
+16 -4
View File
@@ -244,10 +244,22 @@ class AdminService implements AdminServiceInterface
$user->card += $diff; $user->card += $diff;
$user->save(); $user->save();
$addCard->num_card = $newNumCard; $targetRecord = AddCard::where('buyer', $addCard->buyer)
$addCard->seller = $data['seller']; ->where('seller', $data['seller'])
$addCard->date = $data['date']; ->whereDate('date', $data['date'])
$addCard->save(); ->where('id', '!=', $addCard->id)
->first();
if ($targetRecord) {
$targetRecord->num_card += $newNumCard;
$targetRecord->save();
$addCard->delete();
} else {
$addCard->num_card = $newNumCard;
$addCard->seller = $data['seller'];
$addCard->date = $data['date'];
$addCard->save();
}
}); });
} }
+65 -12
View File
@@ -1,28 +1,81 @@
# Hướng dẫn thiết lập Cron Job cho Laravel trên Ubuntu Server # Hướng dẫn thiết lập Cron Job cho Laravel trên Server (Ubuntu)
Để hệ thống tự động chạy các tác vụ định kỳ (bao gồm tác vụ tự động reset thẻ vào cuối tháng qua lệnh `cards:reset`), bạn cần cấu hình Cron Job trên hệ điều hành Ubuntu Server trỏ đến bộ lập lịch (Task Scheduler) của Laravel. Để hệ thống tự động thực hiện các tác vụ định kỳ (bao gồm việc tự động reset thẻ vào lúc 23:59 ngày cuối tháng thông qua lệnh `cards:reset`), bạn cần cấu hình Cron Job trên hệ điều hành Ubuntu Server.
## Các bước thiết lập Dưới đây là hướng dẫn chi tiết cho cả hai trường hợp: **Chạy qua Docker Compose** (mặc định của dự án) và **Chạy trực tiếp trên Host (Không dùng Docker)**.
1. **Mở cấu hình Crontab:** ---
Truy cập vào terminal của máy chủ Ubuntu dưới quyền của user chạy web (thường là `www-data` hoặc user deploy của bạn) và chạy lệnh:
## TRƯỜNG HỢP 1: Chạy qua Docker Compose (Khuyên dùng cho dự án này)
Vì dự án chạy trong container Docker, Cron Job trên server Ubuntu (máy Host) cần gọi lệnh của Laravel vào bên trong container `app`.
### Các bước thiết lập:
1. **Mở file cấu hình Crontab của Ubuntu:**
Mở terminal của Ubuntu Server và chạy lệnh:
```bash ```bash
crontab -e crontab -e
``` ```
2. **Thêm cấu hình chạy Scheduler:** 2. **Thêm cấu hình chạy Laravel Scheduler:**
Thêm dòng dưới đây vào cuối file cấu hình crontab để gọi Laravel scheduler kiểm tra tác vụ mỗi phút: Thêm dòng dưới đây vào cuối file crontab:
```bash ```bash
* * * * * cd /đường-dẫn-đến-thư-mục-dự-án && php artisan schedule:run >> /dev/null 2>&1 * * * * * cd /đường-dẫn-đến-thư-mục-dự-án && docker compose -f docker/docker-compose.yml exec -T app php artisan schedule:run >> /dev/null 2>&1
``` ```
*Lưu ý:* Thay thế `/đường-dẫn-đến-thư-mục-dự-án` bằng đường dẫn tuyệt đối đến thư mục chứa dự án trên server (Ví dụ: `/var/www/thankcard-system`). *Lưu ý quan trọng:*
- Thay `/đường-dẫn-đến-thư-mục-dự-án` thành đường dẫn tuyệt đối đến dự án trên server (Ví dụ: `/var/www/thankcard-system`).
- Tham số **`-T`** (trong `exec -T`) là bắt buộc để tắt chế độ TTY (Tương tác nhập liệu), giúp cron chạy ngầm thành công mà không bị lỗi TTY error.
3. **Lưu và Thoát:** 3. **Lưu và Thoát:**
- Nếu bạn dùng trình soạn thảo `nano`: Nhấn `Ctrl + O` -> nhấn `Enter` để lưu, sau đó bấm `Ctrl + X` để thoát. - Nếu bạn dùng trình soạn thảo `nano`: Nhấn `Ctrl + O` -> nhấn `Enter` để lưu, sau đó bấm `Ctrl + X` để thoát.
- Hệ thống sẽ hiển thị thông báo `crontab: installing new crontab`.
4. **Kiểm tra trạng thái:** ---
Chạy lệnh sau để hiển thị danh sách các cron job đang hoạt động trên user đó:
## TRƯỜNG HỢP 2: Chạy trực tiếp trên Ubuntu Host (Nếu deploy dạng truyền thống)
Nếu bạn cấu hình chạy PHP và Web Server (Nginx/Apache) trực tiếp trên Ubuntu mà không qua Docker.
### Các bước thiết lập:
1. **Mở cấu hình Crontab:**
Truy cập terminal của Ubuntu Server dưới quyền của user chạy Web (thường là `www-data` hoặc user deploy) và chạy lệnh:
```bash
crontab -e
```
2. **Thêm cấu hình chạy Laravel Scheduler:**
Thêm dòng dưới đây vào cuối file crontab:
```bash
* * * * * cd /đường-dẫn-đến-thư-mục-dự-án && php artisan schedule:run >> /dev/null 2>&1
```
*Lưu ý:* Thay `/đường-dẫn-đến-thư-mục-dự-án` bằng đường dẫn tuyệt đối đến thư mục chứa dự án.
3. **Lưu và Thoát.**
---
## Hướng dẫn kiểm tra trạng thái Cron Job
1. **Xem danh sách cron job đang hoạt động:**
```bash ```bash
crontab -l crontab -l
``` ```
2. **Kiểm tra danh sách các tác vụ đã lên lịch trong Laravel:**
```bash
# Nếu dùng Docker:
docker compose -f docker/docker-compose.yml exec -T app php artisan schedule:list
# Nếu không dùng Docker:
php artisan schedule:list
```
3. **Chạy thử scheduler ngay lập tức bằng tay để kiểm thử:**
```bash
# Nếu dùng Docker:
docker compose -f docker/docker-compose.yml exec -T app php artisan schedule:run
# Nếu không dùng Docker:
php artisan schedule:run
```
@@ -4,13 +4,16 @@
<div class="w-16 h-16 rounded-full overflow-hidden border border-gray-200 shrink-0"> <div class="w-16 h-16 rounded-full overflow-hidden border border-gray-200 shrink-0">
<img src="{{ ($user && $user->avatar) ? asset($user->avatar) : asset('images/avatar.png') }}" alt="Avatar" class="w-full h-full object-cover" id="avatar-preview" onerror="this.onerror=null; this.src='{{ asset('images/avatar.png') }}';"> <img src="{{ ($user && $user->avatar) ? asset($user->avatar) : asset('images/avatar.png') }}" alt="Avatar" class="w-full h-full object-cover" id="avatar-preview" onerror="this.onerror=null; this.src='{{ asset('images/avatar.png') }}';">
</div> </div>
<div class="flex-1"> @if($mode !== 'edit')
<label class="block text-[13px] font-bold text-gray-700 mb-1" for="avatar">Thay đổi ảnh đại diện</label> <div class="flex-1">
<input type="file" name="avatar" id="avatar" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-3 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-[#3462f7] file:text-white hover:file:bg-[#254edb]" onchange="previewAvatar(event)"> <label class="block text-[13px] font-bold text-gray-700 mb-1" for="avatar">Thay đổi ảnh đại diện</label>
@error('avatar')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror <input type="file" name="avatar" id="avatar" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-3 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-[#3462f7] file:text-white hover:file:bg-[#254edb]" onchange="previewAvatar(event)">
</div> @error('avatar')<span class="text-red-500 text-[13px] font-semibold mt-1.5 block">{{ $message }}</span>@enderror
</div>
@endif
</div> </div>
@if($mode !== 'edit')
@once @once
@push('scripts') @push('scripts')
<script> <script>
@@ -27,3 +30,4 @@
</script> </script>
@endpush @endpush
@endonce @endonce
@endif
@@ -69,11 +69,6 @@
Bạn chắc chắn muốn cho nhân viên {{ $user->msnv }} nghỉ việc không? Tài khoản này sẽ không thể đăng nhập hoặc thực hiện các giao dịch thẻ sau khi xác nhận. Bạn chắc chắn muốn cho nhân viên {{ $user->msnv }} nghỉ việc không? Tài khoản này sẽ không thể đăng nhập hoặc thực hiện các giao dịch thẻ sau khi xác nhận.
</p> </p>
@else @else
<!-- 1. User Avatar Section -->
@if($isSelf || $isEdit)
<x-user-avatar-section :mode="$mode" :user="$user" />
@endif
<!-- 2. User Basic Information Section --> <!-- 2. User Basic Information Section -->
<x-user-basic-info :mode="$mode" :user="$user" /> <x-user-basic-info :mode="$mode" :user="$user" />
+69 -3
View File
@@ -743,7 +743,7 @@ class AdminUserListStatsTest extends TestCase
'buyer' => 9002, 'buyer' => 9002,
'num_card' => 5, 'num_card' => 5,
'seller' => 9001, 'seller' => 9001,
'date' => Carbon::now()->format('Y-m-d'), 'date' => Carbon::now()->startOfMonth()->toDateString(),
]); ]);
// Transaction 2 (newest) // Transaction 2 (newest)
@@ -751,14 +751,14 @@ class AdminUserListStatsTest extends TestCase
'buyer' => 9002, 'buyer' => 9002,
'num_card' => 10, 'num_card' => 10,
'seller' => 9001, 'seller' => 9001,
'date' => Carbon::now()->format('Y-m-d'), 'date' => Carbon::now()->startOfMonth()->addDay()->toDateString(),
]); ]);
// Try to update older Transaction 1: change num_card from 5 to 6 (increase of 1) // Try to update older Transaction 1: change num_card from 5 to 6 (increase of 1)
$response = $this->actingAs($admin)->put(route('admin.add_cards.update', $oldAddCard->id), [ $response = $this->actingAs($admin)->put(route('admin.add_cards.update', $oldAddCard->id), [
'num_card' => 6, 'num_card' => 6,
'seller' => 9001, 'seller' => 9001,
'date' => Carbon::now()->format('Y-m-d'), 'date' => Carbon::now()->startOfMonth()->toDateString(),
]); ]);
$response->assertRedirect(); $response->assertRedirect();
@@ -771,6 +771,72 @@ class AdminUserListStatsTest extends TestCase
$this->assertEquals(16, $member->card); $this->assertEquals(16, $member->card);
} }
public function test_update_add_card_merges_records_on_same_date(): void
{
$admin = User::create([
'msnv' => 9001,
'name' => 'Admin User',
'mail' => 'admin@example.com',
'pass' => md5('password'),
'departments' => 1,
'role' => config('constants.ROLE_ADMIN'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 0,
'flag_send' => config('constants.FLAG_SEND_DISABLED'),
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
]);
$member = User::create([
'msnv' => 9002,
'name' => 'Member User',
'mail' => 'member@example.com',
'pass' => md5('password'),
'departments' => 1,
'role' => config('constants.ROLE_MEMBER'),
'status' => config('constants.STATUS_ACTIVE'),
'card' => 15,
'flag_send' => config('constants.FLAG_SEND_ENABLED'),
'first_login' => config('constants.FIRST_LOGIN_FALSE'),
]);
// Record A on day 1
$recordA = \App\Models\AddCard::create([
'buyer' => 9002,
'num_card' => 5,
'seller' => 9001,
'date' => Carbon::now()->startOfMonth()->toDateString(),
]);
// Record B on day 10
$recordB = \App\Models\AddCard::create([
'buyer' => 9002,
'num_card' => 10,
'seller' => 9001,
'date' => Carbon::now()->startOfMonth()->addDays(9)->toDateString(),
]);
// Update Record B: set date to day 1, and set num_card to 10
$response = $this->actingAs($admin)->put(route('admin.add_cards.update', $recordB->id), [
'num_card' => 10,
'seller' => 9001,
'date' => Carbon::now()->startOfMonth()->toDateString(),
]);
$response->assertRedirect();
$response->assertSessionHasNoErrors();
// Record B should be deleted (merged)
$this->assertNull(\App\Models\AddCard::find($recordB->id));
// Record A should have 15 cards (5 + 10)
$recordA->refresh();
$this->assertEquals(15, $recordA->num_card);
// Member's card balance remains 15
$member->refresh();
$this->assertEquals(15, $member->card);
}
public function test_delete_add_card_successfully_updates_balance(): void public function test_delete_add_card_successfully_updates_balance(): void
{ {
$admin = User::create([ $admin = User::create([