.
All checks were successful
Build Docker Image / Build Docker Image (push) Successful in 1m15s
Build Docker Image / Restart Docker Compose (push) Successful in 1s

This commit is contained in:
x2Skyz
2025-11-30 23:47:47 +07:00
parent fc8332f25b
commit 5ad8079465
4 changed files with 81 additions and 35 deletions

View File

@@ -1,8 +1,9 @@
import { GeneralService } from '../share/generalservice.js'; import { GeneralService } from '../share/generalservice.js';
import { sendError } from '../utils/response.js'; import { sendError } from '../utils/response.js';
import { verifyToken } from '../utils/token.js'; // ✅ เพิ่ม verifyToken
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import archiver from 'archiver'; // ⚠️ ต้อง npm install archiver import archiver from 'archiver';
export class projectDownload { export class projectDownload {
@@ -10,65 +11,104 @@ export class projectDownload {
this.generalService = new GeneralService(); this.generalService = new GeneralService();
} }
// GET Route ไม่รับ Body ดังนั้นรับ Parameter จาก Query String หรือ Params
async onNavigate(req, res) { async onNavigate(req, res) {
this.generalService.devhint(1, 'projectDownload.js', 'onNavigate() start'); this.generalService.devhint(1, 'projectDownload.js', 'onNavigate() start');
// รับ prjseq จาก Query String (เช่น /projectdownload?prjseq=123)
const prjseq = req.query.prjseq; const prjseq = req.query.prjseq;
const docType = req.query.docType;
// ✅ 1. แกะ Token เพื่อหา Database Schema (เพราะต้อง Query DB)
let token = req.headers.authorization?.split(' ')[1];
let database = 'dbo'; // Default
if (token) {
const decoded = verifyToken(token);
if (decoded && decoded.organization) {
database = decoded.organization;
}
}
if (!prjseq) { if (!prjseq) {
return res.status(400).json(sendError('กรุณาระบุ prjseq', 'Missing prjseq parameter')); return res.status(400).json(sendError('กรุณาระบุ prjseq', 'Missing prjseq parameter'));
} }
return await this.onProjectDownload(req, res, prjseq); return await this.onProjectDownload(req, res, prjseq, docType, database);
} }
async onProjectDownload(req, res, prjseq) { async onProjectDownload(req, res, prjseq, docType, database) {
try { try {
const folderPath = `uploads/projects/${prjseq}`; // ✅ 2. Query เช็คค่า prjdoc ใน Database ก่อน (Source of Truth)
const sql = `SELECT prjdoc FROM ${database}.prjmst WHERE prjseq = $1`;
const result = await this.generalService.executeQueryParam(database, sql, [prjseq]);
// 1. ตรวจสอบว่า Folder มีอยู่จริงหรือไม่ // ถ้าไม่เจอโครงการเลย
if (!fs.existsSync(folderPath)) { if (result.length === 0) {
return res.status(404).json(sendError('ไม่พบเอกสารของโครงการนี้', 'Project documents not found')); return res.json(sendError('ไม่พบข้อมูลโครงการนี้ในระบบ', 'Project not found in DB', 404));
} }
// 2. ตั้งค่า Response Header ให้เป็นไฟล์ Zip const prjdoc = result[0].prjdoc;
const archive = archiver('zip', {
zlib: { level: 9 } // บีบอัดสูงสุด // ✅ 3. เช็คว่า prjdoc ว่างหรือไม่? (null, undefined, หรือ string ว่าง)
if (!prjdoc || prjdoc.trim() === '') {
return res.json(sendError('ไม่พบเอกสารแนบในระบบ (prjdoc ว่าง)', 'No documents recorded in database', 404));
}
// ✅ 4. แปลงรายชื่อไฟล์จาก DB (Comma Separated) เป็น Array
// ตัวอย่าง: "file1.jpg,file2.pdf" -> ["file1.jpg", "file2.pdf"]
let dbFiles = prjdoc.split(',').map(f => f.trim()).filter(f => f !== '');
const folderPath = `uploads/projects/${prjseq}`;
// เช็คว่ามีโฟลเดอร์จริงไหม
if (!fs.existsSync(folderPath)) {
return res.json(sendError('ไม่พบไฟล์ใน Server (โฟลเดอร์สูญหาย)', 'Project folder missing on server', 404));
}
// ✅ 5. กรองไฟล์: ต้องมีชื่อใน DB **และ** มีไฟล์อยู่จริงบน Disk
// (และผ่าน filter docType ถ้ามี)
let validFiles = dbFiles.filter(filename => {
// Filter docType (ถ้าส่งมา)
if (docType && !filename.toLowerCase().endsWith(`.${docType.toLowerCase()}`)) {
return false;
}
// เช็คว่าไฟล์มีอยู่จริงไหม
const fullPath = path.join(folderPath, filename);
return fs.existsSync(fullPath);
}); });
// ถ้ากรองแล้วไม่เหลือไฟล์เลย
if (validFiles.length === 0) {
return res.json(sendError('ไม่พบไฟล์เอกสารที่สามารถดาวน์โหลดได้', 'No valid files found', 404));
}
// --- เริ่มกระบวนการ Zip ---
const archive = archiver('zip', { zlib: { level: 9 } });
const zipFilename = `project_${prjseq}_documents.zip`; const zipFilename = `project_${prjseq}_documents.zip`;
res.attachment(zipFilename); // บอก Browser ว่าให้ Download เป็นชื่อนี้ res.attachment(zipFilename);
// 3. Handle Events
archive.on('error', function(err) { archive.on('error', function(err) {
console.error('Zip Error:', err); console.error('Archiver Error:', err);
res.status(500).send({error: err.message}); if (!res.headersSent) {
res.status(500).send({error: err.message});
}
}); });
// เมื่อบีบอัดเสร็จและส่งข้อมูลครบ
archive.on('end', function() {
console.log(`Archive wrote ${archive.pointer()} bytes`);
});
// 4. Pipe Archive -> Response (Stream)
archive.pipe(res); archive.pipe(res);
// 5. เอาไฟล์ใน Folder ใส่ Zip validFiles.forEach(filename => {
// name: false หมายถึงเอาไฟล์ใส่ root ของ zip เลย ไม่ต้องซ้อน folder const filePath = path.join(folderPath, filename);
archive.directory(folderPath, false); archive.file(filePath, { name: filename });
});
// 6. สั่งจบการบีบอัด (จะเริ่มส่งข้อมูลทันที)
await archive.finalize(); await archive.finalize();
// ⚠️ หมายเหตุ: เราไม่ return อะไรกลับไป เพราะเรา Pipe Stream ใส่ res โดยตรงแล้ว
// globalResponseHandler จะไม่ทำงานเพราะเราไม่ได้ res.json() ซึ่งถูกต้องแล้วสำหรับการโหลดไฟล์
} catch (error) { } catch (error) {
console.error('Download Controller Error:', error); console.error('Download Controller Error:', error);
return res.status(500).json(sendError('เกิดข้อผิดพลาดในการดาวน์โหลด', 'Download Error')); if (!res.headersSent) {
return res.json(sendError('เกิดข้อผิดพลาดในการดาวน์โหลด', 'Download Error', 500));
}
} }
} }
} }

View File

@@ -34,7 +34,14 @@ export class projectSearch {
if (columnParams == 'user') { if (columnParams == 'user') {
let column = `prjseq, prjnam, prjwntbdg, prjcomstt` let column = `prjseq, prjnam, prjwntbdg, prjcomstt, prjacpdtm,
(
SELECT trnacpdtm
FROM ${database}.trnmst t
WHERE trnprjseq = prjseq
ORDER BY trnacpdtm DESC
LIMIT 1
) as trnacpdtm`
condition['prjusrseq'] = req.body.request.prjusrseq || decoded.id condition['prjusrseq'] = req.body.request.prjusrseq || decoded.id
// เรียก Service ตัวเดิม (Simple) // เรียก Service ตัวเดิม (Simple)
aryResult = await this.projectSearchService.getProjectSearch(database, column, condition); aryResult = await this.projectSearchService.getProjectSearch(database, column, condition);

View File

@@ -121,10 +121,9 @@ export class BudgetExpenseService {
UPDATE ${database}.prjmst UPDATE ${database}.prjmst
SET prjacpbdg = $1, SET prjacpbdg = $1,
prjcomstt = $2, prjcomstt = $2,
prjacpdtm = $3 WHERE prjseq = $3
WHERE prjseq = $4
`; `;
await client.query(sqlUpdatePrj, [formattedTotal, projectStatus, currentDTM, projectSeq]); await client.query(sqlUpdatePrj, [formattedTotal, projectStatus, projectSeq]);
await client.query('COMMIT'); await client.query('COMMIT');
return { return {

View File

@@ -34,7 +34,7 @@ export class ProjectSearchService {
const sql = ` const sql = `
SELECT ${selectCol} SELECT ${selectCol}
FROM ${database}.prjmst p FROM ${database}.prjmst p
LEFT JOIN ${database}.usrmst u ON prjusrseq = usrseq LEFT JOIN nuttakit.usrmst u ON prjusrseq = usrseq
LEFT JOIN ${database}.bdgmst b ON prjbdgcod = bdgcod LEFT JOIN ${database}.bdgmst b ON prjbdgcod = bdgcod
WHERE 1=1 WHERE 1=1
ORDER BY prjseq ASC ORDER BY prjseq ASC