443 lines
17 KiB
Markdown
443 lines
17 KiB
Markdown
|
|
# รายงานการตรวจสอบ Unhandled Exception และ Crash Loop
|
||
|
|
## Batch 12: Controllers 111-120
|
||
|
|
|
||
|
|
**วันที่ตรวจสอบ:** 2026-05-08
|
||
|
|
**จำนวน Controllers ที่ตรวจสอบ:** 10 Controllers
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Controllers ที่ตรวจสอบในชุดนี้
|
||
|
|
|
||
|
|
1. [ProfileInsigniaController.ts](src/controllers/ProfileInsigniaController.ts)
|
||
|
|
2. [ProfileInsigniaEmployeeController.ts](src/controllers/ProfileInsigniaEmployeeController.ts)
|
||
|
|
3. [ProfileInsigniaEmployeeTempController.ts](src/controllers/ProfileInsigniaEmployeeTempController.ts)
|
||
|
|
4. [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts)
|
||
|
|
5. [ProfileLeaveEmployeeController.ts](src/controllers/ProfileLeaveEmployeeController.ts)
|
||
|
|
6. [ProfileLeaveEmployeeTempController.ts](src/controllers/ProfileLeaveEmployeeTempController.ts)
|
||
|
|
7. [ProfileNopaidController.ts](src/controllers/ProfileNopaidController.ts)
|
||
|
|
8. [ProfileNopaidEmployeeController.ts](src/controllers/ProfileNopaidEmployeeController.ts)
|
||
|
|
9. [ProfileNopaidEmployeeTempController.ts](src/controllers/ProfileNopaidEmployeeTempController.ts)
|
||
|
|
10. [ProfileOtherController.ts](src/controllers/ProfileOtherController.ts)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## รายการปัญหาที่พบ
|
||
|
|
|
||
|
|
### 1. 🔴 CRITICAL - ProfileInsigniaController.ts - Unhandled Promise in editInsignia
|
||
|
|
|
||
|
|
**File & Location:** [ProfileInsigniaController.ts](src/controllers/ProfileInsigniaController.ts:192-197) - `editInsignia()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
this.insigniaRepo.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
this.insigniaHistoryRepo.save(history, { data: req });
|
||
|
|
}
|
||
|
|
```
|
||
|
|
- มีการเรียก `this.insigniaRepo.save()` และ `this.insigniaHistoryRepo.save()` โดยไม่มี `await` หรือการจัดการ error
|
||
|
|
- ถ้าเกิด error จากการ save database จะทำให้เกิด **Unhandled Promise Rejection**
|
||
|
|
- ไม่มี try-catch รองรับ
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
await this.insigniaRepo.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
await this.insigniaHistoryRepo.save(history, { data: req });
|
||
|
|
}
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating insignia:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2. 🔴 CRITICAL - ProfileInsigniaEmployeeController.ts - Unhandled Promise in editInsignia
|
||
|
|
|
||
|
|
**File & Location:** [ProfileInsigniaEmployeeController.ts](src/controllers/ProfileInsigniaEmployeeController.ts:200-205) - `editInsignia()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
this.insigniaRepo.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
this.insigniaHistoryRepo.save(history, { data: req });
|
||
|
|
}
|
||
|
|
```
|
||
|
|
- มีการเรียก `this.insigniaRepo.save()` และ `this.insigniaHistoryRepo.save()` โดยไม่มี `await`
|
||
|
|
- ถ้า database save ล้มเหลวจะเกิด **Unhandled Promise Rejection**
|
||
|
|
- Data inconsistency อาจเกิดขึ้นถ้า history save ไม่สำเร็จ
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
await this.insigniaRepo.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
await this.insigniaHistoryRepo.save(history, { data: req });
|
||
|
|
}
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating employee insignia:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3. 🔴 CRITICAL - ProfileInsigniaEmployeeTempController.ts - Unhandled Promise in editInsignia
|
||
|
|
|
||
|
|
**File & Location:** [ProfileInsigniaEmployeeTempController.ts](src/controllers/ProfileInsigniaEmployeeTempController.ts:189-194) - `editInsignia()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
this.insigniaRepo.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
this.insigniaHistoryRepo.save(history, { data: req });
|
||
|
|
}
|
||
|
|
```
|
||
|
|
- ไม่มีการ await หรือจัดการ error สำหรับ database operations
|
||
|
|
- ถ้าเกิด error จะทำให้เกิด **Unhandled Promise Rejection** และอาจ crash service
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
await this.insigniaRepo.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
await this.insigniaHistoryRepo.save(history, { data: req });
|
||
|
|
}
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating temp employee insignia:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการบันทึกข้อมูลเครื่องราชอิสริยาภรณ์'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 4. 🔴 CRITICAL - ProfileLeaveController.ts - Unhandled Promise in editLeave
|
||
|
|
|
||
|
|
**File & Location:** [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts:312) - `updateCancel()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
@Patch("cancel/{leaveId}")
|
||
|
|
public async updateCancel(
|
||
|
|
@Request() req: RequestWithUser,
|
||
|
|
@Path() leaveId: string,
|
||
|
|
) {
|
||
|
|
const record = await this.leaveRepo.findOneBy({ leaveId: leaveId }); // ❌ ใช้ leaveId แทน id
|
||
|
|
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
|
||
|
|
// ...
|
||
|
|
```
|
||
|
|
- **BUG**: ใช้ `leaveId` ใน `findOneBy({ leaveId: leaveId })` แต่ column ที่ถูกต้องควรเป็น `id`
|
||
|
|
- ถ้าไม่พบข้อมูลจะ throw HttpError แต่ถ้า database error จะเกิด unhandled exception
|
||
|
|
- ไม่มี try-catch ครอบ database operations
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
@Patch("cancel/{leaveId}")
|
||
|
|
public async updateCancel(
|
||
|
|
@Request() req: RequestWithUser,
|
||
|
|
@Path() leaveId: string,
|
||
|
|
) {
|
||
|
|
try {
|
||
|
|
const record = await this.leaveRepo.findOneBy({ id: leaveId }); // ✅ ใช้ id แทน leaveId
|
||
|
|
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
|
||
|
|
|
||
|
|
const before = structuredClone(record);
|
||
|
|
record.status = "cancel";
|
||
|
|
record.lastUpdateUserId = req.user.sub;
|
||
|
|
record.lastUpdateFullName = req.user.name;
|
||
|
|
record.lastUpdatedAt = new Date();
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
this.leaveRepo.save(record, { data: req }),
|
||
|
|
setLogDataDiff(req, { before, after: record }),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof HttpError) throw error;
|
||
|
|
console.error('Error canceling leave:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการยกเลิกการลา'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 5. 🔴 CRITICAL - ProfileLeaveEmployeeTempController.ts - Unhandled Promises
|
||
|
|
|
||
|
|
**File & Location:** [ProfileLeaveEmployeeTempController.ts](src/controllers/ProfileLeaveEmployeeTempController.ts:132-134) - `newLeave()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
await this.leaveRepo.save(data); // ❌ ไม่มี { data: req } context
|
||
|
|
history.profileLeaveId = data.id; // ❌ ใช้ data.id ที่อาจยังไม่ถูกต้องถ้า save ไม่สำเร็จ
|
||
|
|
await this.leaveHistoryRepo.save(history); // ❌ ไม่มี { data: req } context
|
||
|
|
```
|
||
|
|
- ไม่มี error handling รอบ database operations
|
||
|
|
- การไม่ใส่ `{ data: req }` อาจทำให้ audit trail ไม่สมบูรณ์
|
||
|
|
- ถ้า `leaveRepo.save()` ล้มเหลว จะเกิด unhandled rejection
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
await this.leaveRepo.save(data, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: data });
|
||
|
|
|
||
|
|
history.profileLeaveId = data.id;
|
||
|
|
await this.leaveHistoryRepo.save(history, { data: req });
|
||
|
|
|
||
|
|
return new HttpSuccess(data.id);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error creating employee temp leave:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการบันทึกข้อมูลการลา'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 6. 🟡 HIGH - ProfileNopaidController.ts - Unhandled Promise in editNopaid
|
||
|
|
|
||
|
|
**File & Location:** [ProfileNopaidController.ts](src/controllers/ProfileNopaidController.ts:133-137) - `editNopaid()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
this.nopaidRepository.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
this.nopaidHistoryRepository.save(history, { data: req });
|
||
|
|
}
|
||
|
|
```
|
||
|
|
- ไม่มี `await` สำหรับ database save operations
|
||
|
|
- ถ้าเกิด error จะเป็น **Unhandled Promise Rejection**
|
||
|
|
- ไม่มี try-catch ครอบ
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
await this.nopaidRepository.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
await this.nopaidHistoryRepository.save(history, { data: req });
|
||
|
|
}
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating nopaid:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 7. 🟡 HIGH - ProfileNopaidEmployeeController.ts - Unhandled Promise in editNopaid
|
||
|
|
|
||
|
|
**File & Location:** [ProfileNopaidEmployeeController.ts](src/controllers/ProfileNopaidEmployeeController.ts:140-144) - `editNopaid()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
this.nopaidRepository.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
this.nopaidHistoryRepository.save(history, { data: req });
|
||
|
|
}
|
||
|
|
```
|
||
|
|
- ไม่มีการ await database save operations
|
||
|
|
- ถ้าเกิด error จะทำให้เกิด unhandled promise rejection
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
await this.nopaidRepository.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
await this.nopaidHistoryRepository.save(history, { data: req });
|
||
|
|
}
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating employee nopaid:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 8. 🟡 HIGH - ProfileNopaidEmployeeTempController.ts - Unhandled Promise in editNopaid
|
||
|
|
|
||
|
|
**File & Location:** [ProfileNopaidEmployeeTempController.ts](src/controllers/ProfileNopaidEmployeeTempController.ts:137-141) - `editNopaid()` method
|
||
|
|
|
||
|
|
**Problem Type:** 1. Unhandled Exception
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
this.nopaidRepository.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
this.nopaidHistoryRepository.save(history, { data: req });
|
||
|
|
}
|
||
|
|
```
|
||
|
|
- ไม่มี `await` สำหรับ database operations
|
||
|
|
- Unhandled promise rejection อาจเกิดขึ้น
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
await this.nopaidRepository.save(record, { data: req });
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
|
||
|
|
if (!(Object.keys(body).length === 1 && body.isUpload)) {
|
||
|
|
await this.nopaidHistoryRepository.save(history, { data: req });
|
||
|
|
}
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error updating temp employee nopaid:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการบันทึกข้อมูลบันทึกวันที่ไม่ได้รับเงินเดือน'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 9. 🟢 MEDIUM - ProfileLeaveController.ts - Missing Permission Check in updateCancel
|
||
|
|
|
||
|
|
**File & Location:** [ProfileLeaveController.ts](src/controllers/ProfileLeaveController.ts:308-328) - `updateCancel()` method
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
@Patch("cancel/{leaveId}")
|
||
|
|
public async updateCancel(
|
||
|
|
@Request() req: RequestWithUser,
|
||
|
|
@Path() leaveId: string,
|
||
|
|
) {
|
||
|
|
const record = await this.leaveRepo.findOneBy({ leaveId: leaveId });
|
||
|
|
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
|
||
|
|
|
||
|
|
const before = structuredClone(record);
|
||
|
|
record.status = "cancel";
|
||
|
|
// ... ❌ ไม่มี permission check
|
||
|
|
```
|
||
|
|
- Method `updateCancel` ไม่มีการ check permission ก่อนทำการ cancel
|
||
|
|
- ผู้ใช้ที่ไม่มีสิทธิ์อาจสามารถ cancel การลาของคนอื่นได้
|
||
|
|
- เมื่อเทียบกับ methods อื่นๆ ที่มี permission check ถือว่าเป็นความไม่สอดคล้อง
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
@Patch("cancel/{leaveId}")
|
||
|
|
public async updateCancel(
|
||
|
|
@Request() req: RequestWithUser,
|
||
|
|
@Path() leaveId: string,
|
||
|
|
) {
|
||
|
|
try {
|
||
|
|
const record = await this.leaveRepo.findOneBy({ id: leaveId });
|
||
|
|
if (!record) throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลการลา");
|
||
|
|
|
||
|
|
// ✅ เพิ่ม permission check
|
||
|
|
await new permission().PermissionOrgUserUpdate(
|
||
|
|
req,
|
||
|
|
"SYS_REGISTRY_OFFICER",
|
||
|
|
record.profileId
|
||
|
|
);
|
||
|
|
|
||
|
|
const before = structuredClone(record);
|
||
|
|
record.status = "cancel";
|
||
|
|
record.lastUpdateUserId = req.user.sub;
|
||
|
|
record.lastUpdateFullName = req.user.name;
|
||
|
|
record.lastUpdatedAt = new Date();
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
this.leaveRepo.save(record, { data: req }),
|
||
|
|
setLogDataDiff(req, { before, after: record }),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return new HttpSuccess();
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof HttpError) throw error;
|
||
|
|
console.error('Error canceling leave:', error);
|
||
|
|
throw new HttpError(
|
||
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||
|
|
'เกิดข้อผิดพลาดในการยกเลิกการลา'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## สรุปประเด็นสำคัญ
|
||
|
|
|
||
|
|
### ปัญหาที่พบเป็นพื้นฐานซ้ำๆ:
|
||
|
|
1. **Unhandled Promise Rejections** - การเรียก database save methods โดยไม่มี `await` ใน methods แก้ไขข้อมูล (edit/update)
|
||
|
|
2. **Missing Try-Catch Blocks** - การขาด error handling รอบ database operations
|
||
|
|
3. **Data Consistency Risks** - การบันทึก history โดยไม่รู้ว่า main record บันทึกสำเร็จหรือไม่
|
||
|
|
4. **Bug in updateCancel** - การใช้ `leaveId` แทน `id` ใน findOneBy
|
||
|
|
|
||
|
|
### คำแนะนำในการแก้ไข:
|
||
|
|
1. เพิ่ม try-catch ครอบทุก database operations ที่เสี่ยงต่อการเกิด error
|
||
|
|
2. ใช้ `await` กับทุก promise ที่เกี่ยวกับ database save/update
|
||
|
|
3. เพิ่ม permission check ใน method `updateCancel`
|
||
|
|
4. แก้ไข bug การใช้ `leaveId` ใน findOneBy ให้เป็น `id`
|
||
|
|
5. พิจารณาใช้ Transaction สำหรับการบันทึกข้อมูลที่ต้องการความสอดคล้องกัน (main record + history)
|
||
|
|
|
||
|
|
### การประเมินความเสี่ยง:
|
||
|
|
- 🔴 **CRITICAL**: 4 จุด - อาจทำให้เกิด Unhandled Exception และ Crash Loop
|
||
|
|
- 🟡 **HIGH**: 4 จุด - อาจทำให้เกิด Unhandled Exception
|
||
|
|
- 🟢 **MEDIUM**: 1 จุด - ปัญหาความปลอดภัยและความสอดคล้องของระบบ
|