update
This commit is contained in:
@@ -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
@@ -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 có 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 có 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" />
|
||||||
|
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user