260322:1648 Correct Coresspondence / Doing RFA / Correct CI
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
# **BACKEND DEVELOPMENT**
|
||||
|
||||
## **Phase 2: High-Integrity Data & File Management**
|
||||
|
||||
### **T2.1 CommonModule**
|
||||
|
||||
|
||||
### **T2.2 FileStorageService - Two-Phase Storage**
|
||||
|
||||
|
||||
ระบบนี้ออกแบบมาเพื่อแก้ปัญหา "ไฟล์ขยะ" (Orphan Files) ที่เกิดจากการอัปโหลดแล้ว User ไม่กดยืนยัน โดยเราจะแบ่งการทำงานเป็น 2 เฟส:
|
||||
|
||||
1. **Upload (Temp):** เอาไฟล์ไปพักไว้ก่อน (ยังไม่ลง DB ถาวร)
|
||||
2. **Commit (Permanent):** เมื่อ User กด Save ฟอร์มสำเร็จ ค่อยย้ายไฟล์ไปเก็บจริง
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### ขั้นตอนที่ 1: ติดตั้ง Libraries ที่จำเป็น
|
||||
|
||||
@@ -111,7 +111,7 @@ import { Attachment } from './entities/attachment.entity.js';
|
||||
export class FileStorageModule {}
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### 📥 งานถัดไป (สำคัญมาก)
|
||||
|
||||
@@ -149,14 +149,12 @@ export class FileStorageService {
|
||||
constructor(
|
||||
@InjectRepository(Attachment)
|
||||
private attachmentRepository: Repository<Attachment>,
|
||||
private configService: ConfigService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
// ใช้ Path จริงถ้าอยู่บน Server (Production) หรือใช้ ./uploads ถ้าอยู่ Local
|
||||
this.uploadRoot =
|
||||
this.configService.get('NODE_ENV') === 'production'
|
||||
? '/share/dms-data'
|
||||
: path.join(process.cwd(), 'uploads');
|
||||
|
||||
this.configService.get('NODE_ENV') === 'production' ? '/share/dms-data' : path.join(process.cwd(), 'uploads');
|
||||
|
||||
// สร้างโฟลเดอร์รอไว้เลยถ้ายังไม่มี
|
||||
fs.ensureDirSync(path.join(this.uploadRoot, 'temp'));
|
||||
}
|
||||
@@ -215,7 +213,7 @@ export class FileStorageService {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear().toString();
|
||||
const month = (today.getMonth() + 1).toString().padStart(2, '0');
|
||||
|
||||
|
||||
// โฟลเดอร์ถาวรแยกตาม ปี/เดือน
|
||||
const permanentDir = path.join(this.uploadRoot, 'permanent', year, month);
|
||||
await fs.ensureDir(permanentDir);
|
||||
@@ -227,7 +225,7 @@ export class FileStorageService {
|
||||
try {
|
||||
// ย้ายไฟล์
|
||||
await fs.move(oldPath, newPath, { overwrite: true });
|
||||
|
||||
|
||||
// อัปเดตข้อมูลใน DB
|
||||
att.filePath = newPath;
|
||||
att.isTemporary = false;
|
||||
@@ -251,7 +249,7 @@ export class FileStorageService {
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
##### 2. สร้าง `FileStorageController` (Endpoint สำหรับ Upload)
|
||||
|
||||
@@ -290,10 +288,10 @@ export class FileStorageController {
|
||||
// ตรวจสอบประเภทไฟล์ (Regex)
|
||||
new FileTypeValidator({ fileType: /(pdf|msword|openxmlformats|zip|octet-stream)/ }),
|
||||
],
|
||||
}),
|
||||
})
|
||||
)
|
||||
file: Express.Multer.File,
|
||||
@Request() req,
|
||||
@Request() req
|
||||
) {
|
||||
// ส่ง userId จาก Token ไปด้วย
|
||||
return this.fileStorageService.upload(file, req.user.userId);
|
||||
@@ -301,20 +299,20 @@ export class FileStorageController {
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
##### 3. ทดสอบด้วย Postman
|
||||
|
||||
1. **Start Server:** `pnpm start:dev`
|
||||
2. **Login:** ขอ Token ของ Admin หรือ User มาก่อน
|
||||
3. **สร้าง Request ใหม่:**
|
||||
* **Method:** `POST`
|
||||
* **URL:** `http://localhost:3000/api/files/upload`
|
||||
* **Auth:** Bearer Token
|
||||
* **Body:**
|
||||
* เลือก `form-data`
|
||||
* Key: `file` (เลือกชนิดเป็น **File**) -\> เลือกไฟล์ PDF หรือรูปภาพสักไฟล์
|
||||
* **Send**
|
||||
- **Method:** `POST`
|
||||
- **URL:** `http://localhost:3000/api/files/upload`
|
||||
- **Auth:** Bearer Token
|
||||
- **Body:**
|
||||
- เลือก `form-data`
|
||||
- Key: `file` (เลือกชนิดเป็น **File**) -\> เลือกไฟล์ PDF หรือรูปภาพสักไฟล์
|
||||
- **Send**
|
||||
|
||||
**ผลลัพธ์ที่ควรได้:**
|
||||
JSON ตอบกลับที่มีข้อมูลไฟล์ และ `isTemporary: true`, `tempId: "..."` ครับ
|
||||
@@ -371,10 +369,10 @@ export class FileStorageController {
|
||||
new MaxFileSizeValidator({ maxSize: 50 * 1024 * 1024 }), // 50MB
|
||||
new FileTypeValidator({ fileType: /(pdf|msword|openxmlformats|zip|octet-stream)/ }),
|
||||
],
|
||||
}),
|
||||
})
|
||||
)
|
||||
file: Express.Multer.File,
|
||||
@Request() req: RequestWithUser, // ✅ 2. ระบุ Type ตรงนี้แทน any
|
||||
@Request() req: RequestWithUser // ✅ 2. ระบุ Type ตรงนี้แทน any
|
||||
) {
|
||||
return this.fileStorageService.upload(file, req.user.userId);
|
||||
}
|
||||
@@ -407,7 +405,7 @@ Error แจ้งว่า `Cannot GET ...` แสดงว่าคุณก
|
||||
|
||||
เราจะใช้เทคนิค **Double-Lock** ตามแผน: **Redis Lock (ด่านแรก)** + **Optimistic Lock (ด่านสุดท้าย)**
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### ขั้นตอนที่ 1: ติดตั้ง Redis Client
|
||||
|
||||
@@ -420,7 +418,7 @@ pnpm add ioredis redlock
|
||||
pnpm add -D @types/ioredis
|
||||
```
|
||||
|
||||
*(หมายเหตุ: `redlock` เวอร์ชันล่าสุดอาจรวมอยู่ใน ioredis หรือใช้ library แยก ตรวจสอบ version compatibility ด้วยครับ แต่วิธีมาตรฐานคือลงแยก)*
|
||||
_(หมายเหตุ: `redlock` เวอร์ชันล่าสุดอาจรวมอยู่ใน ioredis หรือใช้ library แยก ตรวจสอบ version compatibility ด้วยครับ แต่วิธีมาตรฐานคือลงแยก)_
|
||||
|
||||
#### ขั้นตอนที่ 2: สร้าง Module และ Entities
|
||||
|
||||
@@ -436,7 +434,7 @@ nest g service modules/document-numbering
|
||||
#### ขั้นตอนที่ 3: สร้าง Entities
|
||||
|
||||
สร้างไฟล์: `src/modules/document-numbering/entities/document-number-format.entity.ts`
|
||||
*(เก็บ Template เช่น `{ORG}-{TYPE}-{SEQ:4}`)*
|
||||
_(เก็บ Template เช่น `{ORG}-{TYPE}-{SEQ:4}`)_
|
||||
|
||||
```typescript
|
||||
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn, Unique } from 'typeorm';
|
||||
@@ -467,7 +465,7 @@ export class DocumentNumberFormat {
|
||||
```
|
||||
|
||||
สร้างไฟล์: `src/modules/document-numbering/entities/document-number-counter.entity.ts`
|
||||
*(เก็บเลขล่าสุด)*
|
||||
_(เก็บเลขล่าสุด)_
|
||||
|
||||
```typescript
|
||||
import { Entity, Column, PrimaryColumn, VersionColumn } from 'typeorm';
|
||||
@@ -491,8 +489,8 @@ export class DocumentNumberCounter {
|
||||
lastNumber!: number;
|
||||
|
||||
// ✨ หัวใจสำคัญของ Optimistic Lock
|
||||
@VersionColumn()
|
||||
version!: number;
|
||||
@VersionColumn()
|
||||
version!: number;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -508,16 +506,14 @@ import { DocumentNumberFormat } from './entities/document-number-format.entity.j
|
||||
import { DocumentNumberCounter } from './entities/document-number-counter.entity.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([DocumentNumberFormat, DocumentNumberCounter]),
|
||||
],
|
||||
imports: [TypeOrmModule.forFeature([DocumentNumberFormat, DocumentNumberCounter])],
|
||||
providers: [DocumentNumberingService],
|
||||
exports: [DocumentNumberingService], // Export ให้คนอื่นเรียกใช้
|
||||
})
|
||||
export class DocumentNumberingModule {}
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
**งานถัดไป (ยากสุด):** เขียน Logic ใน `DocumentNumberingService` เพื่อ:
|
||||
|
||||
@@ -531,7 +527,7 @@ export class DocumentNumberingModule {}
|
||||
|
||||
ดังนั้น **ผ่าน** ครับ ลุยต่อได้เลย\!
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### 🏗️ งานหลัก: เขียน Logic ใน `DocumentNumberingService`
|
||||
|
||||
@@ -560,7 +556,7 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
private counterRepo: Repository<DocumentNumberCounter>,
|
||||
@InjectRepository(DocumentNumberFormat)
|
||||
private formatRepo: Repository<DocumentNumberFormat>,
|
||||
private configService: ConfigService,
|
||||
private configService: ConfigService
|
||||
) {}
|
||||
|
||||
// 1. เริ่มต้นเชื่อมต่อ Redis และ Redlock เมื่อ Module ถูกโหลด
|
||||
@@ -571,16 +567,13 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
password: this.configService.get<string>('REDIS_PASSWORD'),
|
||||
});
|
||||
|
||||
this.redlock = new Redlock(
|
||||
[this.redisClient],
|
||||
{
|
||||
driftFactor: 0.01,
|
||||
retryCount: 10, // ลองใหม่ 10 ครั้งถ้า Lock ไม่สำเร็จ
|
||||
retryDelay: 200, // รอ 200ms ก่อนลองใหม่
|
||||
retryJitter: 200,
|
||||
}
|
||||
);
|
||||
|
||||
this.redlock = new Redlock([this.redisClient], {
|
||||
driftFactor: 0.01,
|
||||
retryCount: 10, // ลองใหม่ 10 ครั้งถ้า Lock ไม่สำเร็จ
|
||||
retryDelay: 200, // รอ 200ms ก่อนลองใหม่
|
||||
retryJitter: 200,
|
||||
});
|
||||
|
||||
this.logger.log('Redis & Redlock initialized for Document Numbering');
|
||||
}
|
||||
|
||||
@@ -601,11 +594,11 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
orgId: number,
|
||||
typeId: number,
|
||||
year: number,
|
||||
replacements: Record<string, string> = {},
|
||||
replacements: Record<string, string> = {}
|
||||
): Promise<string> {
|
||||
const resourceKey = `doc_num:${projectId}:${typeId}:${year}`;
|
||||
const ttl = 5000; // Lock จะหมดอายุใน 5 วินาที (ป้องกัน Deadlock)
|
||||
|
||||
|
||||
let lock;
|
||||
try {
|
||||
// 🔒 Step 1: Redis Lock (Distributed Lock)
|
||||
@@ -641,19 +634,17 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
// 2.4 ถ้าบันทึกผ่าน -> สร้าง String ตาม Format
|
||||
return await this.formatNumber(projectId, typeId, counter.lastNumber, replacements);
|
||||
|
||||
} catch (err) {
|
||||
// ถ้า Version ชนกัน (Optimistic Lock Error) ให้วนลูปทำใหม่
|
||||
if (err instanceof OptimisticLockVersionMismatchError) {
|
||||
this.logger.warn(`Optimistic Lock Hit! Retrying... (${i + 1}/${maxRetries})`);
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
throw err; // ถ้าเป็น Error อื่น ให้โยนออกไปเลย
|
||||
}
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException('Failed to generate document number after retries');
|
||||
|
||||
throw new InternalServerErrorException('Failed to generate document number after retries');
|
||||
} catch (err) {
|
||||
this.logger.error('Error generating document number', err);
|
||||
throw err;
|
||||
@@ -667,16 +658,16 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
// Helper: แปลงเลขเป็น String ตาม Template (เช่น {ORG}-{SEQ:004})
|
||||
private async formatNumber(
|
||||
projectId: number,
|
||||
typeId: number,
|
||||
seq: number,
|
||||
projectId: number,
|
||||
typeId: number,
|
||||
seq: number,
|
||||
replacements: Record<string, string>
|
||||
): Promise<string> {
|
||||
// 1. หา Template
|
||||
const format = await this.formatRepo.findOne({ where: { projectId, correspondenceTypeId: typeId } });
|
||||
|
||||
|
||||
// ถ้าไม่มี Template ให้ใช้ Default: {SEQ}
|
||||
let template = format ? format.formatTemplate : '{SEQ:4}';
|
||||
let template = format ? format.formatTemplate : '{SEQ:4}';
|
||||
|
||||
// 2. แทนที่ค่าต่างๆ (ORG_CODE, TYPE_CODE, YEAR)
|
||||
for (const [key, value] of Object.entries(replacements)) {
|
||||
@@ -694,7 +685,7 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
**✅ สิ่งที่ Logic นี้ทำ:**
|
||||
|
||||
@@ -716,7 +707,7 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
1. **Rate Limiting:** ป้องกันการโดนยิง API รัวๆ (Brute-force / DDoS) ด้วย `@nestjs/throttler`
|
||||
2. **Security Headers:** ป้องกันการโจมตีพื้นฐานทาง Web (XSS, Clickjacking) ด้วย `helmet`
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### ขั้นตอนที่ 1: ติดตั้ง Libraries
|
||||
|
||||
@@ -756,7 +747,7 @@ import { AuthModule } from './common/auth/auth.module.js';
|
||||
validationSchema: envValidationSchema,
|
||||
validationOptions: { abortEarly: true },
|
||||
}),
|
||||
|
||||
|
||||
// 🛡️ 1. Setup Throttler Module (Rate Limiting)
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
@@ -844,7 +835,7 @@ async function bootstrap() {
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
app.useGlobalInterceptors(new TransformInterceptor());
|
||||
@@ -856,7 +847,7 @@ async function bootstrap() {
|
||||
bootstrap();
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### 💡 วิธีปรับจูน Rate Limit เฉพาะจุด (Optional)
|
||||
|
||||
@@ -887,7 +878,7 @@ import { SkipThrottle } from '@nestjs/throttler';
|
||||
check() { ... }
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### ✅ เสร็จสิ้น T2.4
|
||||
|
||||
@@ -897,7 +888,7 @@ check() { ... }
|
||||
2. **Helmet:** ป้องกัน XSS/Header Exploits
|
||||
3. **CORS:** ควบคุมโดเมนที่เรียกใช้ได้
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### 🚀 T2.5 JSON Schema System (Final Task of Phase 2)
|
||||
|
||||
@@ -912,7 +903,7 @@ Task สุดท้ายของ Phase 2 คือการเตรียม
|
||||
|
||||
ระบบนี้จะช่วยให้เรามั่นใจว่าข้อมูลในฟิลด์ `details` (JSON) ของเอกสารต่างๆ (เช่น RFA, Correspondence) มีโครงสร้างที่ถูกต้องเสมอ ไม่ใช่แค่ Text อะไรก็ได้
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### ขั้นตอนที่ 1: ติดตั้ง Library (AJV)
|
||||
|
||||
@@ -937,7 +928,7 @@ nest g controller modules/json-schema
|
||||
```
|
||||
|
||||
สร้างไฟล์ Entity: `src/modules/json-schema/entities/json-schema.entity.ts`
|
||||
*(อ้างอิงตาม Data Dictionary)*
|
||||
_(อ้างอิงตาม Data Dictionary)_
|
||||
|
||||
```typescript
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
@@ -989,11 +980,11 @@ import { JsonSchema } from './entities/json-schema.entity.js';
|
||||
export class JsonSchemaService implements OnModuleInit {
|
||||
private ajv: Ajv;
|
||||
// Cache ตัว Validator ที่ Compile แล้ว เพื่อประสิทธิภาพ
|
||||
private validators = new Map<string, any>();
|
||||
private validators = new Map<string, any>();
|
||||
|
||||
constructor(
|
||||
@InjectRepository(JsonSchema)
|
||||
private schemaRepo: Repository<JsonSchema>,
|
||||
private schemaRepo: Repository<JsonSchema>
|
||||
) {
|
||||
// ตั้งค่า AJV
|
||||
this.ajv = new Ajv({ allErrors: true, strict: false }); // strict: false เพื่อยืดหยุ่นกับ custom keywords
|
||||
@@ -1013,8 +1004,8 @@ export class JsonSchemaService implements OnModuleInit {
|
||||
|
||||
// ถ้ายังไม่มีใน Cache หรือต้องการตัวล่าสุด ให้ดึงจาก DB
|
||||
if (!validate) {
|
||||
const schema = await this.schemaRepo.findOne({
|
||||
where: { schemaCode, isActive: true }
|
||||
const schema = await this.schemaRepo.findOne({
|
||||
where: { schemaCode, isActive: true },
|
||||
});
|
||||
|
||||
if (!schema) {
|
||||
@@ -1094,7 +1085,7 @@ export class JsonSchemaController {
|
||||
|
||||
// Endpoint สำหรับ Test Validate (Optional)
|
||||
@Post(':code/validate')
|
||||
@RequirePermission('document.view')
|
||||
@RequirePermission('document.view')
|
||||
async validate(@Param('code') code: string, @Body() data: any) {
|
||||
const isValid = await this.schemaService.validate(code, data);
|
||||
return { valid: isValid };
|
||||
@@ -1143,7 +1134,7 @@ import { JsonSchemaModule } from './modules/json-schema/json-schema.module.js';
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
#### ✅ จบ Phase 2 สมบูรณ์แบบครับ\!
|
||||
|
||||
@@ -1160,7 +1151,7 @@ Backend ของเราแข็งแกร่งมากแล้วคร
|
||||
|
||||
ถ้าผ่านแล้ว พิมพ์ **"ปิด Phase 2 เริ่ม Phase 3"** ได้เลยครับ เราจะไปสร้าง "Unified Workflow Engine" กัน\!
|
||||
|
||||
### T2.5 Error**
|
||||
### T2.5 Error\*\*
|
||||
|
||||
ขออภัยครับ ผมลืมใส่บรรทัด **Import** ให้ในตัวอย่างก่อนหน้านี้ครับ 😅
|
||||
|
||||
@@ -1185,12 +1176,9 @@ export class AuthController {
|
||||
|
||||
@Post('login')
|
||||
// จำกัดการ Login: ได้แค่ 5 ครั้ง ใน 60 วินาที (60000 ms) ต่อ IP
|
||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||
async login(@Body() loginDto: LoginDto) {
|
||||
const user = await this.authService.validateUser(
|
||||
loginDto.username,
|
||||
loginDto.password,
|
||||
);
|
||||
const user = await this.authService.validateUser(loginDto.username, loginDto.password);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
@@ -1213,14 +1201,12 @@ export class AuthController {
|
||||
#### ปัญหาเหล่านี้เกิดจาก **Strict Mode** ครับ
|
||||
|
||||
1. **Error TS2564 (Property has no initializer):** ตัวแปร `redisClient` และ `redlock` ถูกประกาศไว้แต่ไม่ได้ใส่ค่าใน `constructor` (เพราะเราไปใส่ใน `onModuleInit` แทน) TypeScript เลยกลัวว่ามันจะเป็น undefined
|
||||
|
||||
* **วิธีแก้:** ใส่เครื่องหมาย **`!`** ต่อท้ายชื่อตัวแปร เพื่อยืนยันว่า "เดี๋ยวมีค่าแน่นอน ไม่ต้องห่วง"
|
||||
- **วิธีแก้:** ใส่เครื่องหมาย **`!`** ต่อท้ายชื่อตัวแปร เพื่อยืนยันว่า "เดี๋ยวมีค่าแน่นอน ไม่ต้องห่วง"
|
||||
|
||||
2. **Error TS7016 (Could not find a declaration file for 'redlock'):** ไลบรารี `redlock` (เวอร์ชันที่เราลง) ไม่มีไฟล์ Type Definition มาให้ หรือ TypeScript หาไม่เจอ
|
||||
- **วิธีแก้:** สร้างไฟล์ประกาศ Type (Declaration File) ขึ้นมาเองเพื่อบอก TypeScript ว่า "รู้จัก Module นี้นะ"
|
||||
|
||||
* **วิธีแก้:** สร้างไฟล์ประกาศ Type (Declaration File) ขึ้นมาเองเพื่อบอก TypeScript ว่า "รู้จัก Module นี้นะ"
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
**🛠️ วิธีแก้ไข**
|
||||
|
||||
@@ -1234,7 +1220,7 @@ export class AuthController {
|
||||
@Injectable()
|
||||
export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(DocumentNumberingService.name);
|
||||
|
||||
|
||||
// ✅ ใส่ ! ต่อท้ายชื่อตัวแปร
|
||||
private redisClient!: Redis;
|
||||
private redlock!: Redlock;
|
||||
@@ -1242,7 +1228,7 @@ export class DocumentNumberingService implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(
|
||||
// ...
|
||||
) {}
|
||||
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
@@ -1340,10 +1326,10 @@ Error `WRONGPASS` ชัดเจนว่าพยายามเชื่อ
|
||||
เปิดไฟล์ `docker-compose.yml` แล้วดูบรรทัด `command` ของ service `redis` ว่าตั้งรหัสผ่านว่าอะไร
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
# ...
|
||||
# ดูตรงคำว่า "redis_password_secure" (หรือค่าที่คุณตั้งไว้)
|
||||
command: redis-server --requirepass "redis_password_secure"
|
||||
redis:
|
||||
# ...
|
||||
# ดูตรงคำว่า "redis_password_secure" (หรือค่าที่คุณตั้งไว้)
|
||||
command: redis-server --requirepass "redis_password_secure"
|
||||
```
|
||||
|
||||
##### 2. เช็คไฟล์ `.env`
|
||||
@@ -1379,8 +1365,8 @@ pnpm start:dev
|
||||
|
||||
คราวนี้น่าจะเชื่อมต่อผ่านแล้วครับ\! (ถ้ายังไม่ได้ ลองเช็คว่าเผลอมี Redis ตัวอื่นรันซ้อนอยู่หรือเปล่าด้วย `docker ps` ครับ)
|
||||
|
||||
-----
|
||||
---
|
||||
|
||||
## ปิด Phase 2 เริ่ม Phase 3
|
||||
|
||||
ถ้าแก้ไขผ่านแล้ว พิมพ์ **"ปิด Phase 2 เริ่ม Phase 3"** ได้เลยครับ เราจะไปลุย **Unified Workflow Engine** ที่เป็นหัวใจสำคัญของระบบใน Phase 3 กันครับ\!
|
||||
ถ้าแก้ไขผ่านแล้ว พิมพ์ **"ปิด Phase 2 เริ่ม Phase 3"** ได้เลยครับ เราจะไปลุย **Unified Workflow Engine** ที่เป็นหัวใจสำคัญของระบบใน Phase 3 กันครับ\!
|
||||
|
||||
Reference in New Issue
Block a user