#process#report#custom-sql#template#security#postgres
SQL กำหนดเองและเทมเพลตรายงาน (Custom SQL & report templates)
ผู้ดูแลระบบแก้ SQL ของรายงานเองได้ (Report On-The-Fly) และบันทึก "รูปแบบรายงาน" (ชื่อ/หัวเรื่อง/SQL/คอลัมน์/ตั้งค่าหน้ากระดาษ) ลงตาราง erp_report_template ของฐานข้อมูลบริษัท ส่วน SQL ที่ผู้ใช้เขียนถูกกรองให้เป็นอ่านอย่างเดียวและรันใน transaction แบบ read-only
ทำงานอย่างไร
เส้นทางการรัน Custom SQL
RequiredRightเห็นcustomSqlไม่ว่าง -> ต้องสิทธิ์admin(การตรวจสิทธิ์เมนูของ route process (Process route rights))RunReportรันรายงานมาตรฐานของเมนูก่อนเพื่อรู้คอลัมน์/DefaultSQLแล้วถ้ามีcustomSqlเรียกcustomSQLRefusal: ปฏิเสธเมื่อcostHidden("ไม่มีสิทธิ์ดูต้นทุนสินค้า...") หรือเมื่อรายงานเป็นpermGuarded("รายงานนี้จำกัดสิทธิ์คลัง/ที่เก็บ ไม่อนุญาตแก้ SQL") เหตุผลตามคอมเมนต์: Custom SQL อ่านตารางตรงจึงข้ามกฎรายผู้ใช้ที่icRunใช้runCustomSQLReport:validateReadOnlyReportSQL: ตัด comment, ตัด;ท้าย, ต้องขึ้นต้นด้วยSELECTหรือWITH, ห้ามมี;นอก string literal (hasUnquotedSemicolon), ตัด string literal แบบ'...'ออกก่อนตรวจคำต้องห้าม- คำต้องห้าม (regex
forbiddenSQLKeywordsRe, ไม่สนตัวพิมพ์):INSERT,UPDATE,DELETE,DROP,ALTER,TRUNCATE,GRANT,REVOKE,CREATE,COPY,CALL,EXECUTE,VACUUM,REINDEX,CLUSTER,LOCK,DISCARD,SET ROLE,SET SESSION, ฟังก์ชันอ่านไฟล์/เซิร์ฟเวอร์ (PG_READ_FILE,PG_LS_*,LO_*,DBLINK*,PG_SLEEP,PG_TERMINATE_BACKEND,SET_CONFIG,CURRENT_SETTING,QUERY_TO_XMLฯลฯ) และตารางระบบ (PG_AUTHID,PG_SHADOWฯลฯ) expandNamedReportParamsแทนที่พารามิเตอร์ชื่อ::date_from/:from_date/{{date_from}},:date_to/:to_date/{{date_to}},:from_code/{{from_code}},:to_code/{{to_code}},:kw_like/{{kw_like}},:keyword/:search/{{keyword}}ด้วย literal ที่หนี'เป็น''(toSQLLiteral) และแทน$1..$6ที่หลงเหลือจากการคัดลอก SQL (ลำดับ: วันเริ่ม, วันสิ้นสุด, keyword, kwLike, จากรหัส, ถึงรหัส)- ถ้า SQL ไม่มีคำ
LIMITจะห่อด้วยSELECT * FROM (...) AS _sml_otf_q LIMIT 50000 - รันใน
pool.BeginTxด้วยAccessMode: pgx.ReadOnly(ReadCommitted) แล้ว rollback เสมอ - สร้าง
ReportColumnMetaจากชื่อคอลัมน์ผลลัพธ์ (ชื่อซ้ำ/ว่าง/?column?ถูกตั้งชื่อคอลัมน์_N), จับคู่คอลัมน์เดิมของรายงานเมื่อ PostgreSQL ตัด alias ที่ยาวเกิน 63 ไบต์, ตรวจชนิดตัวเลขจาก OID (isPostgresNumericOID)
- ผลที่ได้แทนที่ผลมาตรฐาน แต่คง
UsesDateRange,CodeRangeLabel,Conditionsเดิม และExecutedSQLบันทึก SQL ที่รันจริง
เทมเพลตรายงาน (erp_report_template)
- ตารางสร้างเองเมื่อจำเป็น (
ensureReportTemplateTable,CREATE TABLE IF NOT EXISTS): คอลัมน์template_id(PK),menu_key,template_name,description,is_default,is_shared,custom_title,custom_subtitle,custom_sql,columns_json(jsonb),page_setup_json(jsonb),created_by,updated_by, เวลา; มีดัชนีที่menu_key; ในคำสั่งเดียวกันยังสร้างตารางformdesign(แบบฟอร์ม) ถ้ายังไม่มี listReportTemplates: อ่านเทมเพลตที่menu_key = <เมนู>หรือmenu_key = '*'(ใช้ร่วมทุกเมนู) เรียงis_default DESC, updated_at DESC, template_name; ถ้าตารางสร้างไม่ได้ (DB อ่านอย่างเดียว) ไม่ถือเป็น error คืนรายการว่างexecuteTemplateMutation(จากtemplateAction):save:template_idว่าง -> สร้างrtpl_<hex 8 ไบต์>; ชื่อว่าง -> "รูปแบบกำหนดเอง dd/mm hh:mm"; ถ้าtemplateIsDefaultล้างค่า default ของเมนูก่อน; upsertON CONFLICT (template_id)โดยมีเงื่อนไขWHERE menu_key = EXCLUDED.menu_key(id ของรายงานอื่นจะไม่ถูกเขียนทับ ->KindNotFound); ทั้งหมดใน transaction- พิเศษสำหรับเมนู
menu_form_edit/menu_import_formdesign: เขียนแถวลงformdesignด้วย (formcode=customSubtitleตัดไม่เกิน 25 ตัวอักษร,formdesigntext= ค่าในfooterNote) หลังลบแถวเดิมที่formcode/guid_codeตรงกัน delete: ลบตามtemplate_id+menu_key; ไม่พบ ->KindNotFoundset_default: ล้าง default ทั้งเมนู แล้วตั้งตัวใหม่ (rollback ถ้า id ไม่อยู่ในเมนูนี้)- ผู้บันทึกคือ
Username(ว่าง =system);menuKeyว่างใช้default
- ข้อมูลที่เก็บใน
ReportTemplateDTO:columns=[]ReportColumnOverride(key,label,visible,widthPercent,align,isNumeric,isSummable,aggregate,order) และpageSetup=ReportPageSetup(กระดาษ, แนว, ขนาดฟอนต์, ธีม, ความหนาแน่น, เส้นตาราง, ลายม้าลาย, ลายเซ็น,footerNote,groupByColumn,subGroupByColumn)
จุดที่ต้องระวัง
- เมนูตัวออกแบบ (
fast_report_designer,menu_form_edit,menu_import_formdesign) ไม่มีรายงานมาตรฐานให้รัน ใช้RunReportเพื่อบันทึก/ลบเทมเพลตและรัน SQL ของตัวเองเท่านั้น (report.goค่าคงที่ท้ายไฟล์) - ผู้ใช้ที่ไม่มีสิทธิ์ดูต้นทุนยังบันทึกเทมเพลตของตัวออกแบบได้ เพราะเมนูนี้ไม่ผ่านกฎต้นทุน
- ห่อ
LIMIT 50000ใช้เฉพาะเมื่อ SQL ไม่มีคำว่าLIMIT(ตรวจหลังตัด string literal) - เทมเพลตผูกกับ
menu_key: จะ save/delete/set_default ข้ามเมนูไม่ได้
ที่มาในโค้ด
backend/internal/application/process/report_template.go:38-86-ReportColumnOverride,ReportPageSetup,ReportTemplateDTObackend/internal/application/process/report_template.go:198-ensureReportTemplateTable(ตารางerp_report_template,formdesign)backend/internal/application/process/report_template.go:237-generateTemplateID(rtpl_+ hex)backend/internal/application/process/report_template.go:243-listReportTemplatesbackend/internal/application/process/report_template.go:338-executeTemplateMutationbackend/internal/application/process/report_template.go:499-forbiddenSQLKeywordsRebackend/internal/application/process/report_template.go:502-validateReadOnlyReportSQLbackend/internal/application/process/report_template.go:606-expandNamedReportParamsbackend/internal/application/process/report_template.go:644-runCustomSQLReport(LIMIT 50000, ReadOnly tx)backend/internal/application/process/report.go:380-customSQLRefusalbackend/internal/application/process/report.go:393-402- ชื่อเมนูตัวออกแบบ (reportDesignerMenuKeyฯลฯ)
ดูเพิ่ม
- เครื่องมือรายงานฝั่ง Backend (Report engine / RunReport)
- ทะเบียนรายงานรายเมนูและเงื่อนไขกรอง (Menu report registry & conditions)
- การจัดกลุ่มหลายระดับและการพิมพ์ PDF รายงาน (Report hierarchy & PDF)
- การตรวจสิทธิ์เมนูของ route process (Process route rights)
- ภาพรวมกลุ่ม process ของ Backend (Process overview)