MyWellnetPTN Logo
MyWellnetPTN
Gateway API Documentation
Developer Hub
จัดการ API Key (Admin) หน้าหลักพอร์ทัล

คู่มือการเชื่อมต่อ Discord Bot Gateway & API

ระบบเกตเวย์ส่งการแจ้งเตือนทางเดียวแบบรวมศูนย์ (Centralized One-Way Discord Notification Gateway) ทำหน้าที่ส่งข้อความแจ้งเตือนหาเจ้าหน้าที่ (Direct Message) หรือประกาศลงห้อง Discord Channel โดยตรง เชื่อมโยงบัญชี SSO และเลขบัตรประชาชนอัตโนมัติ พร้อมระบบความปลอดภัยและการควบคุมสิทธิ์ระดับสูง

ข้อกำหนดสถาปัตยกรรมระบบปัจจุบัน (One-Way Notification Policy):
• ไม่รองรับปฏิกิริยารีแอ็คชัน (No Reactions): บอทจะไม่ใส่อีโมจิหรือรับรีแอ็คชันใดๆ จากผู้ใช้
• ไม่รองรับการตอบกลับใดๆ (No Replies / No Inbound Chat): บอทจะไม่ประมวลผลข้อความแชทตอบกลับใน DM หรือการพิมพ์คุยกับบอท เพื่อความปลอดภัยและป้องกันสแปม
• ทำได้เฉพาะการส่งข้อความหา User ทางเดียว (Outbound Dispatch Only): ส่ง Text, Rich Embed และปุ่มประเภท LINK (สำหรับคลิกเปิดหน้าเว็บระบบภายนอกเท่านั้น)

1. แม่แบบคำสั่งสำหรับป้อนให้ AI ของระบบลูก (AI Integration Prompt)

หากท่านกำลังพัฒนาระบบลูก (เช่น ระบบระบาดวิทยา Epitrack, ระบบแผนยุทธศาสตร์ หรือระบบรายงาน KPI) และใช้ AI ช่วยเขียนโค้ด (Cursor, Copilot, ChatGPT) สามารถคัดลอกข้อความด้านล่างนี้ไปให้ AI เรียนรู้เพื่อเขียนฟังก์ชันส่งแจ้งเตือนได้ทันที:

System Prompt Template สำหรับ AI
คุณเป็นผู้เชี่ยวชาญด้านการเชื่อมต่อ API กำลังพัฒนาระบบส่งแจ้งเตือน Discord ผ่าน "MyWellnetPTN Discord Gateway" ของกองสาธารณสุข องค์การบริหารส่วนจังหวัดปัตตานี ระบบเกตเวย์นี้เป็นระบบส่งการแจ้งเตือนทางเดียว (One-Way Outbound Notification) โดยมีข้อกำหนดทางเทคนิคสำคัญดังนี้: 1. ข้อกำหนดและนโยบายของระบบ (System Policy): - ระบบทำได้เฉพาะส่งข้อความหา User หรือ Channel ทางเดียวเท่านั้น (One-Way Notification) - ไม่รองรับปฏิกิริยารีแอ็คชัน (No Reactions) และไม่รับการตอบกลับข้อความใดๆ จากผู้ใช้ (No Replies / Inbound Chat) - ปุ่มใน actions รองรับเฉพาะสไตล์ "LINK" เพื่อเปิด URL ไปยังเว็บระบบงานภายนอกเท่านั้น 2. การยืนยันตัวตน (Authentication): - ส่งผ่าน Header: X-Client-ID: [ใส่ Client ID ที่ได้จากแอดมิน] X-Client-Secret: [ใส่ Client Secret ที่ได้จากแอดมิน] Content-Type: application/json 3. Endpoint สำหรับส่งแจ้งเตือน: - URL: [APP_BASE_URL]/api/v1/notify (ยึดตามตัวแปรสภาพแวดล้อม APP_BASE_URL หรือ DISCORD_GATEWAY_URL ในไฟล์ .env) - Method: POST - ระบุผู้รับผ่าน target_citizen_id (เลขบัตร ปชช. 13 หลัก) หรือ target_email หรือ target_discord_id - ระบบปลายทางจะแปลงข้อมูลเป็น Discord User ID ผ่านฐานข้อมูล SSO ให้อัตโนมัติ 4. โครงสร้าง JSON Payload ตัวอย่าง: { "target_citizen_id": "1949900123456", "bot_id": "default", "embed": { "title": "🚨 แจ้งเตือนเคสระบาดวิทยาใหม่ (Epitrack)", "description": "พบผู้ป่วยสงสัยโรคไข้เลือดออก 1 ราย ในพื้นที่ รพ.สต.บานา กรุณาตรวจสอบ", "color": "#EF4444", "fields": [ { "name": "รหัสเคส", "value": "EPI-2026-0042", "inline": true }, { "name": "ความรุนแรง", "value": "ระดับ 3 (เฝ้าระวังพิเศษ)", "inline": true }, { "name": "ผู้ป่วย", "value": "ชายไทย อายุ 24 ปี", "inline": false } ], "footer": { "text": "ระบบระบาดวิทยา Epitrack • กองสาธารณสุข อบจ.ปัตตานี" } }, "actions": [ { "label": "เปิดดูรายละเอียดเคสบนเว็บ", "style": "LINK", "url": "https://epitrack.mywellnetptn.org/cases/0042" } ] } หน้าที่ของคุณ: โปรดเขียนฟังก์ชันส่งแจ้งเตือนแบบ One-Way ในระบบของเรา พร้อมคำนึงถึง Error Handling และการจัดการ Timeout อย่างเหมาะสม

2. สถาปัตยกรรมและความปลอดภัย (Architecture & Security)

ระบบเกตเวย์ทำหน้าที่เป็น Outbound Notification Hub ตัวกลางระหว่างเว็บแอปพลิเคชันสาธารณสุขกับ Discord โดยมีมาตรการรักษาความปลอดภัยระดับสูง:

�ทยาใหม่ (Epitrack)", "description": "พบผู้ป่วยสงสัยโรคไข้เลือดออก 1 ราย ในพื้นที่ รพ.สต.บานา", "color": "#EF4444", "fields": [ { "name": "รหัสเคส", "value": "EPI-2026-0042", "inline": true }, { "name": "ความรุนแรง", "value": "ระดับ 3 (เฝ้าระวังพิเศษ)", "inline": true }, { "name": "ผู้ป่วย", "value": "ชายไทย อายุ 24 ปี", "inline": false } ], "footer": { "text": "ระบบระบาดวิทยา Epitrack • กองสาธารณสุข อบจ.ปัตตานี" } }, "actions": [ { "label": "รับเคสนี้", "style": "SUCCESS", "action_id": "accept_case_0042", "emoji": "✅" }, { "label": "ส่งต่อทีมสอบสวน", "style": "PRIMARY", "action_id": "escalate_case_0042", "emoji": "📋" }, { "label": "เปิดดูรายละเอียดเคส", "style": "LINK", "url": "https://epitrack.mywellnetptn.org/cases/0042" } ] } 4. การรับผลลัพธ์ผ่าน Webhook: เมื่อเจ้าหน้าที่กดปุ่มใน Discord เกตเวย์จะยิง HTTP POST กลับมายัง webhook_url ที่ผูกไว้กับ Client ID พร้อม Header `X-Gateway-Signature` ที่เป็น HMAC-SHA256(payload, client_secret) เพื่อความปลอดภัยสูงสุด หน้าที่ของคุณ: โปรดเขียนฟังก์ชันส่งแจ้งเตือนในระบบของเรา พร้อมคำนึงถึง Error Handling และฟังก์ชันรับ Webhook ตรวจสอบ HMAC Signature

2. สถาปัตยกรรมและความปลอดภัย (Architecture & Security)

ระบบเกตเวย์ทำหน้าที่เป็น Hub ตัวกลางระหว่างเว็บแอปพลิเคชันสาธารณสุขกับ Discord โดยมีมาตรการรักษาความปลอดภัยระดับสูง:

3. รายละเอียด API Endpoints

POST

/api/v1/notify — ส่งการแจ้งเตือนทางเดียว (Outbound Notification)

ส่งข้อความหาเจ้าหน้าที่แบบส่วนตัว (Direct Message) หรือส่งเข้าห้องดิสคอร์ด

POST https://[HOST_URL]/api/v1/notify

พารามิเตอร์ Request Body (JSON):

ฟิลด์ ชนิด ความจำเป็น คำอธิบาย
target_citizen_id String อย่างใดอย่างหนึ่ง เลขประจำตัวประชาชน 13 หลักของผู้รับ (ระบบจะดึง Discord ID อัตโนมัติ)
target_email String อย่างใดอย่างหนึ่ง อีเมลที่ผูกไว้กับ SSO
target_discord_id String อย่างใดอย่างหนึ่ง Discord User Snowflake ID 18-19 หลักของผู้รับโดยตรง
bot_id String ทางเลือก ระบุบอทที่ต้องการใช้ส่ง เช่น portal, epitrack, strat, kpi (ค่าเริ่มต้น: บอทที่ผูกไว้)
content String ทางเลือก ข้อความธรรมดา (Plain text) เช่น ข้อความแจ้งเตือนด่วน
embed Object ทางเลือก Rich Embed Card สวยงาม (title, description, color, fields, footer)
actions Array ทางเลือก ปุ่มกดลิงก์ภายนอก (รองรับเฉพาะสไตล์ LINK สำหรับเปิดหน้าเว็บระบบงานภายนอก • ปัจจุบันไม่รองรับปุ่มกดโต้ตอบ/รีแอ็คชัน)
GET

/api/v1/users/lookup — ตรวจสอบการผูกบัญชี SSO กับ Discord

ตรวจสอบว่าบุคลากรคนดังกล่าวได้ยืนยันตัวตนในระบบ Discord หรือยัง ก่อนที่จะส่งการแจ้งเตือน

GET https://[HOST_URL]/api/v1/users/lookup?citizen_id=1949900123456
GET

/api/v1/bots/status — ดูสถานะคลัสเตอร์บอท

ตรวจสอบสถานะความพร้อม (Online / Standby / Latency) ของบอททุกตัวในเครือข่าย

GET https://[HOST_URL]/api/v1/bots/status

4. Official Drop-in SDK (ไฟล์เดียวจบ ไม่ต้องติดตั้ง Library เพิ่ม)

ดาวน์โหลดไฟล์ SDK ภาษาที่ระบบลูกของท่านใช้งาน วางไว้ในโปรเจกต์ แล้ว Require/Import ใช้งานได้ทันที มีฟังก์ชันครอบคลุมทั้งส่งแจ้งเตือน ตรวจสอบ SSO และรับ Webhook Verification:

PHP Standalone SDK

ไฟล์เดียวจบ MyWellnetGateway.php ไม่ต้องมี Composer รองรับ PHP 7.4 - 8.3+ เหมาะสำหรับทั้งเว็บดั้งเดิมและ Laravel

ดาวน์โหลด PHP SDK

Node.js SDK

ไฟล์เดียวจบ mywellnet-gateway.js ใช้ Native Fetch & Crypto ไม่ต้องมี dependencies เพิ่ม พร้อม Express Webhook Middleware

ดาวน์โหลด Node.js SDK

Python SDK

ไฟล์เดียวจบ mywellnet_gateway.py ใช้ Standard Library ล้วน (urllib/hmac) ไม่ต้อง pip install อะไรเลย

ดาวน์โหลด Python SDK

ตัวอย่างโค้ดการเรียกใช้งานและโครงสร้าง Payload

# กำหนดตัวแปร APP_BASE_URL จากไฟล์ .env
curl -X POST "$APP_BASE_URL/api/v1/notify" \
  -H "X-Client-ID: client_epitrack" \
  -H "X-Client-Secret: sec_epi_live_994182" \
  -H "Content-Type: application/json" \
  -d '{
    "target_citizen_id": "1949900123456",
    "embed": {
      "title": "🚨 มีเคสต้องสงสัยโรคติดต่อใหม่ในพื้นที่",
      "description": "พบผู้ป่วยสงสัยไข้เลือดออก 1 ราย ในเขต รพ.สต.บานา กรุณาตรวจสอบและดำเนินการ",
      "color": "#EF4444",
      "fields": [
        { "name": "รหัสเคส", "value": "EPI-2026-0089", "inline": true },
        { "name": "ระดับความรุนแรง", "value": "เฝ้าระวังเร่งด่วน", "inline": true }
      ],
      "footer": { "text": "ระบบ Epitrack • กองสาธารณสุข อบจ.ปัตตานี" }
    },
    "actions": [
      { "label": "รับเคสนี้ทันที", "style": "SUCCESS", "action_id": "accept_epi_0089", "emoji": "✅" },
      { "label": "ดูรายละเอียดในเว็ป", "style": "LINK", "url": "https://epitrack.mywellnetptn.org/cases/0089" }
    ]
  }'
<?php
// ตัวอย่างการส่งแจ้งเตือนผ่านภาษา PHP (cURL)
// ดึง Gateway URL จากตัวแปรสภาพแวดล้อม (.env) เป็นหลัก 100%
$gatewayUrl = rtrim(getenv('DISCORD_GATEWAY_URL') ?: getenv('APP_BASE_URL'), '/') . '/api/v1/notify';
$clientId = 'client_epitrack';
$clientSecret = 'sec_epi_live_994182';

$payload = [
    'target_citizen_id' => '1949900123456',
    'embed' => [
        'title' => '🚨 มีเคสต้องสงสัยโรคติดต่อใหม่ในพื้นที่',
        'description' => 'พบผู้ป่วยสงสัยไข้เลือดออก 1 ราย ในเขต รพ.สต.บานา',
        'color' => '#EF4444',
        'fields' => [
            ['name' => 'รหัสเคส', 'value' => 'EPI-2026-0089', 'inline' => true],
            ['name' => 'ระดับ', 'value' => 'ด่วนมาก', 'inline' => true]
        ],
        'footer' => ['text' => 'ระบบระบาดวิทยา Epitrack • กองสาธารณสุข อบจ.ปัตตานี']
    ],
    'actions' => [
        ['label' => 'รับเคสนี้', 'style' => 'SUCCESS', 'action_id' => 'accept_epi_0089', 'emoji' => '✅'],
        ['label' => 'เปิดดูเคส', 'style' => 'LINK', 'url' => 'https://epitrack.mywellnetptn.org/cases/0089']
    ]
];

$ch = curl_init($gatewayUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-Client-ID: ' . $clientId,
    'X-Client-Secret: ' . $clientSecret
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);
if ($httpCode === 200 && $result['success']) {
    echo "ส่งการแจ้งเตือนสำเร็จ Tracking ID: " . $result['tracking_id'];
} else {
    echo "เกิดข้อผิดพลาด: " . ($result['error'] ?? 'Unknown error');
}
?>
// ตัวอย่างการส่งแจ้งเตือนผ่าน Node.js (Fetch API หรือ Axios)
// ยึด DISCORD_GATEWAY_URL หรือ APP_BASE_URL จาก .env เป็นหลัก 100%
const BASE_URL = (process.env.DISCORD_GATEWAY_URL || process.env.APP_BASE_URL || '').replace(/\/+$/, '');
const GATEWAY_URL = `${BASE_URL}/api/v1/notify`;
const CLIENT_ID = 'client_strat';
const CLIENT_SECRET = 'sec_strat_live_771829';

async function sendDiscordNotification(citizenId, taskData) {
  const payload = {
    target_citizen_id: citizenId,
    embed: {
      title: '📋 แจ้งเตือนการติดตามโครงการตามแผนยุทธศาสตร์',
      description: `โครงการ "${taskData.title}" ถึงกำหนดรายงานผลความก้าวหน้าไตรมาส 2`,
      color: '#3B82F6',
      fields: [
        { name: 'งบประมาณ', value: taskData.budget + ' บาท', inline: true },
        { name: 'กลุ่มงาน', value: taskData.department, inline: true }
      ],
      footer: { text: 'ระบบแผนยุทธศาสตร์สาธารณสุข • กองสาธารณสุข อบจ.ปัตตานี' }
    },
    actions: [
      { label: 'บันทึกความก้าวหน้า', style: 'PRIMARY', action_id: `report_${taskData.id}`, emoji: '📝' },
      { label: 'ดูเอกสารโครงการ', style: 'LINK', url: taskData.viewUrl }
    ]
  };

  const res = await fetch(GATEWAY_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Client-ID': CLIENT_ID,
      'X-Client-Secret': CLIENT_SECRET
    },
    body: JSON.stringify(payload)
  });

  const data = await res.json();
  if (!res.ok || !data.success) {
    throw new Error(data.error || 'Failed to send notification');
  }

  return data; // { success: true, tracking_id: 'mwptn_track_...' }
}
# ตัวอย่างการส่งแจ้งเตือนผ่านภาษา Python (Requests)
# ยึด DISCORD_GATEWAY_URL หรือ APP_BASE_URL จาก .env เป็นหลัก 100%
import os
import requests

BASE_URL = (os.getenv("DISCORD_GATEWAY_URL") or os.getenv("APP_BASE_URL") or "").rstrip("/")
GATEWAY_URL = f"{BASE_URL}/api/v1/notify"
HEADERS = {
    "X-Client-ID": "client_kpi",
    "X-Client-Secret": "sec_kpi_live_441029",
    "Content-Type": "application/json"
}

payload = {
    "target_citizen_id": "1949900123456",
    "embed": {
        "title": "📊 สรุปตัวชี้วัดสาธารณสุขประจำเดือน (KPI Alert)",
        "description": "รายงานตัวชี้วัดความครอบคลุมวัคซีน รพ.สต.บานา ประจำเดือนนี้ผ่านเกณฑ์แล้ว",
        "color": "#10B981",
        "fields": [
            { "name": "ผลงาน", "value": "92.4%", "inline": True },
            { "name": "เป้าหมาย", "value": ">= 90.0%", "inline": True }
        ],
        "footer": { "text": "ระบบตัวชี้วัด KPI • กองสาธารณสุข อบจ.ปัตตานี" }
    },
    "actions": [
        { "label": "รับทราบผลงาน", "style": "SUCCESS", "action_id": "kpi_ack_2026_03", "emoji": "👍" }
    ]
}

response = requests.post(GATEWAY_URL, json=payload, headers=HEADERS)
data = response.json()
print("Result:", data)
// ตัวอย่าง Webhook Endpoint รับการกดปุ่มจาก Discord ในระบบลูก (Express.js)
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

const CLIENT_SECRET = 'sec_epi_live_994182';

app.post('/api/webhook/discord', (req, res) => {
  // 1. ตรวจสอบลายเซ็นดิจิทัล HMAC-SHA256 ป้องกันการปลอมแปลง
  const signature = req.headers['x-gateway-signature'];
  const expectedSig = crypto
    .createHmac('sha256', CLIENT_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expectedSig) {
    return res.status(401).json({ error: 'INVALID_SIGNATURE' });
  }

  // 2. ดึงข้อมูลว่าใครเป็นคนกดปุ่ม
  const { event, action_id, user } = req.body;
  console.log(`เจ้าหน้าที่: ${user.full_name} (ปชช: ${user.citizen_id}) ได้กด: ${action_id}`);

  // 3. ปรับปรุงสถานะเคสในฐานข้อมูลระบบลูก
  // await updateCaseStatus(action_id, user);

  res.json({ success: true, message: 'บันทึกการรับเคสเรียบร้อย' });
});

app.listen(8080, () => console.log('Child Webhook listening on port 8080'));

5. คำสั่งควบคุมบอทสำหรับผู้ดูแลระบบ (Admin Slash Commands) & การกำหนดสิทธิ์ผ่าน MyWellnet Portal

เนื่องจากระบบเกตเวย์ทำงานเป็น One-Way Outbound Notification (ไม่รองรับรีแอ็คชันและการตอบกลับข้อความทั่วไปจาก User) บอทจึงมีชุดคำสั่งควบคุมและดูแลความเรียบร้อยบน Discord (Slash Commands) เฉพาะสำหรับผู้ดูแลระบบเท่านั้น โดยสามารถควบคุมสิทธิ์การใช้งานของแต่ละบุคคลได้อย่างละเอียดผ่านระบบกลาง MyWellnet:

/clear หรือ /ล้างแชท

ล้างข้อความในช่องทางประสานงานที่ระบุ (ตั้งแต่ 1 ถึง 100 ข้อความ) เพื่อรักษาความเป็นระเบียบและลบข้อมูลการแจ้งเตือนเก่าที่เสร็จสิ้นภารกิจแล้ว พร้อมรายงานสรุปจำนวนข้อความที่ถูกลบ

/status หรือ /ตรวจสถานะ

ตรวจสอบความพร้อมของบอทแบบเรียลไทม์: สภาวะตื่น/หลับ (Live Status), WebSocket Latency / Ping (ms), ระยะเวลาออนไลน์ Uptime, ปริมาณการใช้ RAM/Heap, สถานะการเชื่อมต่อฐานข้อมูล MySQL และ Host Environment พร้อมปุ่มกดรีเฟรชข้อมูลสด

/restart หรือ /รีสตาร์ทบอท

สั่งรีสตาร์ทกระบวนการทำงานของบอทอย่างปลอดภัย (Safe Supervisor Restart) ผ่านระบบ Process Supervisor (app.js) โดยไม่กระทบต่อการทำงานของ Web Server หลัก

/stop หรือ /หยุดบอท

สั่งหยุดพักการทำงานของบอท (Safe Standby / Shutdown) โดยระบบ Supervisor จะบันทึกสถานะไว้และไม่รันบอทขึ้นมาใหม่โดยพลการ จนกว่าผู้ดูแลระบบจะสั่งเปิดใช้งานใหม่ผ่านแผงควบคุมหน้าเว็บ Admin Portal

ระบบกำหนดสิทธิ์การใช้คำสั่งบอทผ่าน MyWellnet Admin Portal

ผู้ดูแลระบบสามารถเข้าจัดการสิทธิ์คำสั่งบอทได้ที่หน้า Admin Portal (/admin.html) ผ่านปุ่ม "กำหนดสิทธิ์คำสั่งบอท" โดยมีกลไกความปลอดภัยดังนี้:

  • กำหนดสิทธิ์รายบุคคลและรายคำสั่ง: ระบุ Discord User ID และเลือกได้ว่าจะให้ใช้คำสั่งใดได้บ้าง (all, clear, status, restart, stop) ข้อมูลจะบันทึกในตาราง bot_admin_permissions ทันที
  • การตรวจสอบสิทธิ์แบบหลายระดับ (Multi-tier Security Guard): เมื่อมีการเรียกใช้คำสั่ง ระบบจะตรวจสอบสิทธิ์จาก Environment SuperAdmin → Config Admin → ตารางสิทธิ์กลาง MyWellnet → Administrator ของ Discord Server
  • การปฏิเสธการเข้าถึง (Access Denied Guard): หากสมาชิกทั่วไปหรือผู้ที่ไม่มีสิทธิ์พยายามใช้คำสั่ง บอทจะปฏิเสธการทำงานและส่งการแจ้งเตือนเตือนความปลอดภัยเฉพาะบุคคล (Ephemeral Message) ทันที
  • ความปลอดภัยของโทเค็น (Token Safety): การส่ง Ping หรือ Heartbeat เฝ้าระวังบอท ไม่มีผลทำให้โทเค็นของบอทถูกรีเซ็ต เนื่องจากเป็นเพียงการตรวจสอบสถานะเชื่อมต่อตามมาตรฐาน WebSocket ธรรมดา

6. ระบบส่งประกาศ & บรอดแคสต์ (Discord Broadcast API)

รองรับการส่งข้อความประกาศแจ้งเตือนหมู่ (Bulk Broadcast) เข้าสู่หลายห้อง Discord หรือส่ง Direct Message ถึงบุคลากรหลายคนพร้อมกันตามรายชื่อสังกัด/หน่วยงาน โดยระบบมีกลไก Throttling Delay (250ms) ป้องกันการชน Discord Rate Limit (HTTP 429) โดยอัตโนมัติ

POST /api/v1/broadcast ต้องใช้ X-Client-Id / Secret

โครงสร้าง Request Body (JSON)

{
  "channels": ["1545347262669000754", "1551842442997661716"],
  "departments": ["รพ.สต.บานา", "กองสาธารณสุข"],
  "title": "📢 ประกาศเร่งด่วน: ตรวจสอบรายงานผลการดำเนินงาน",
  "message": "ขอให้หน่วยงานในสังกัดตรวจสอบความถูกต้องของข้อมูลตัวชี้วัดและส่งรายงานภายในวันนี้ 16:30 น.",
  "color": "#2563EB",
  "mention_roles": ["@everyone"],
  "action_url": "https://mywellnet.pattanipao.go.th/portal/",
  "bot_id": "portal"
}

ตัวอย่างการเรียกใช้งานผ่าน cURL

curl -X POST https://mywellnet.pattanipao.go.th/api/v1/broadcast \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: your_client_id" \
  -H "X-Client-Secret: your_client_secret" \
  -d '{
    "channels": ["1545347262669000754"],
    "title": "ประกาศจากศูนย์ข้อมูล",
    "message": "ระบบพร้อมให้บริการแล้ว",
    "color": "#059669"
  }'

ตัวอย่างโค้ด PHP (cURL Implementation)

<?php
$payload = [
    'channels'    => ['1545347262669000754'],
    'departments' => ['รพ.สต.บานา'],
    'title'       => '📢 แจ้งเตือนประชุมด่วน',
    'message'     => 'ขอเชิญประชุมติดตามงาน ณ ห้องประชุม อบจ. เวลา 13:30 น.',
    'color'       => '#7C3AED',
    'action_url'  => 'https://mywellnet.pattanipao.go.th/'
];

$ch = curl_init('https://mywellnet.pattanipao.go.th/api/v1/broadcast');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-Client-Id: ' . $clientId,
        'X-Client-Secret: ' . $clientSecret
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE)
]);

$response = curl_exec($ch);
$result = json_decode($response, true);
echo "ส่งสำเร็จ: " . $result['stats']['success'] . "/" . $result['stats']['total'];
?>

7. ระบบเฝ้าระวังสุขภาพบอท & Webhook Alert (Bot Health Sentinel)

แก้ปัญหาบอทหยุดทำงานโดยไม่ทราบสาเหตุ ด้วยระบบ Health Sentinel ที่ทำหน้าที่เสมือนตัวบอทมีชีวิตและรายงานอาการป่วยของตนเองแบบอัตโนมัติผ่าน Discord Webhook แยกอิสระ ทำให้แม้ว่า WebSocket ของบอทจะหลุด หรือติดปัญหา Token เซิร์ฟเวอร์ก็ยังสามารถแจ้งเตือนผู้ดูแลระบบได้ทันที

🟢 บอทตื่นและพร้อมทำงาน (Ready & Online)

แจ้งเตือนเมื่อบอทล็อกอินและเชื่อมต่อ Shard สำเร็จ พร้อมรายงาน Latency Ping (ms) และรายชื่อเซิร์ฟเวอร์ที่ประจำการอยู่

🔴 บอทปิดตัวลง / พักเครื่อง (Shutdown / Restart)

ดักจับสัญญาณ SIGINT / SIGTERM รายงานเหตุผลการหยุดโปรเซส ไม่ว่าจะเป็นการสั่งอัปเดตระบบ หรือเครื่องเซิร์ฟเวอร์รีสตาร์ต

💥 บั๊คและข้อยกเว้นร้ายแรง (Unhandled Bugs & Crash)

ดักจับ Uncaught Exceptions, Unhandled Rejections และ Interaction Failures พร้อมระบุ Stack Trace และชื่อฟังก์ชันที่มีปัญหา

🚫 ปัญหา Token & Privileged Intents

แจ้งเตือนทันทีเมื่อ Token ไม่ถูกต้อง โทเค็นถูก Reset หรือขาดสิทธิ์ Server Members / Message Content Intents พร้อมขั้นตอนแก้ไขแบบละเอียด

ขั้นตอนการตั้งค่า Discord Webhook สำหรับแจ้งเตือน

  1. เปิด Discord ในเซิร์ฟเวอร์ที่ต้องการ -> เข้าช่องทางสำหรับดูสถานะ เช่น #bot-health-alert
  2. กดไอคอนฟันเฟือง Edit Channel (แก้ไขช่อง) -> ไปที่เมนู Integrations (การรวมระบบ)
  3. คลิก Webhooks -> New Webhook (สร้างเว็บฮุก) -> ตั้งชื่อบอท เช่น Bot Health Doctor
  4. คลิก Copy Webhook URL
  5. เข้าหน้า Admin Portal (/admin.html) -> ที่แถบเครื่องมือด้านบนคลิก "ตรวจสุขภาพบอท & Alert"
  6. วาง Webhook URL ลงในช่อง -> กดปุ่ม "🔔 ส่งข้อความทดสอบ (Test Ping)" เพื่อตรวจสอบว่าข้อความเข้าห้องจริง
  7. เลือกประเภทเหตุการณ์ที่ต้องการให้บอทแจ้งเตือน -> กด "บันทึกการตั้งค่า"

9. แนวทางระบบระบาดวิทยา (EpiTrack Disease Surveillance): Private Tiered DM Only

⚖️ มาตรการความปลอดภัยและความลับทางการแพทย์ (PDPA & Health Data Privacy)

ข้อมูลเคสโรคระบาดและผู้ป่วยโรคติดต่ออันตรายเป็น ข้อมูลสุขภาพที่มีความละเอียดอ่อนสูง (Sensitive Personal Data) ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล พ.ศ. 2562 (PDPA) มาตรา 26, พ.ร.บ. สุขภาพแห่งชาติ พ.ศ. 2550 มาตรา 7 และ พ.ร.บ. โรคติดต่อ พ.ศ. 2558
เกตเวย์กำหนดกฎเหล็ก: ห้ามส่งเคสโรคระบาดเข้าห้องกลุ่มหรือช่อง Discord สาธารณะโดยเด็ดขาด! ต้องส่งแบบ Direct Message (DM) ถึงตัวเจ้าหน้าที่ผู้รับผิดชอบตามสิทธิ์เท่านั้น

1. Strict DM เท่านั้น

กำหนด target_type: "user" และระบุเลขประจำตัวประชาชน 13 หลัก target_citizen_id ของเจ้าหน้าที่ผู้รับผิดชอบ ห้ามใช้ channel เพื่อป้องกันข้อมูลผู้ป่วยรั่วไหลสู่ผู้อื่น

2. ส่งตามสิทธิ์ 3 ลำดับขั้น

ระดับ 1: รพ.สต. พื้นที่เกิดเหตุ เพื่อลงพื้นที่สอบสวนโรคภายใน 24 ชม.
ระดับ 2: สสอ. อำเภอ (ทีม CDCU/SRRT) เพื่อติดตามและสนับสนุนยา/เวชภัณฑ์
ระดับ 3: สสจ. จังหวัด เมื่อเป็นโรคติดต่ออันตรายร้ายแรง หรือเกิด Cluster ระบาด

3. เฉพาะผู้ที่เชื่อม SSO แล้ว

ส่งหาเจ้าหน้าที่ที่ผูกบัญชี SSO กับ Discord แล้วเท่านั้น หากระบบแจ้งเตือนว่ายังไม่ได้ผูก (404 USER_NOT_LINKED) ให้ระบบแสดงข้อความเตือนบนหน้าเว็บและใช้ช่องทางสำรอง

ตัวอย่างโค้ด PHP: การส่งแจ้งเตือนเคสโรคระบาดแบบ Private DM
require_once __DIR__ . '/MyWellnetGateway.php'; // ดึงค่า URL เกตเวย์จากตัวแปรสภาพแวดล้อม APP_BASE_URL ในไฟล์ .env $gw = new MyWellnetGateway(getenv('APP_BASE_URL'), 'client_epitrack', 'sec_epitrack_xxxxxx'); // 1. ตรวจสอบก่อนว่าเจ้าหน้าที่ รพ.สต. ผูกบัญชี Discord SSO หรือยัง $officerCitizenId = '1949900123456'; $lookup = $gw->lookupUser($officerCitizenId); if (!$lookup['success'] || !$lookup['found']) { // ยังไม่ได้เชื่อมโยง Discord SSO -> บันทึกสถานะเพื่อให้ขึ้นเตือนบนหน้า Dashboard error_log("เจ้าหน้าที่ รพ.สต. ($officerCitizenId) ยังไม่ได้เชื่อมโยง Discord SSO"); } else { // 2. ส่ง Direct Message (DM) ถึงตัวเจ้าหน้าที่ พร้อมปุ่มรับเคส $result = $gw->notify([ 'target_type' => 'user', 'target_citizen_id' => $officerCitizenId, 'embed' => [ 'title' => '🚨 แจ้งเตือนเคสสงสัยโรคติดต่ออันตราย (รพ.สต.บานา)', 'description' => 'พบผู้ป่วยเข้าเกณฑ์สอบสวนโรค กรุณาลงพื้นที่ตรวจสอบและควบคุมโรคภายใน 24 ชม.', 'color' => '#DC2626', 'fields' => [ ['name' => 'รหัสเคส', 'value' => 'EPI-2026-089', 'inline' => true], ['name' => 'โรคที่สงสัย', 'value' => 'โรคไข้เลือดออกเดงกี (DHF)', 'inline' => true], ['name' => 'พื้นที่', 'value' => 'หมู่ 2 ต.บานา อ.เมืองปัตตานี', 'inline' => false] ] ], 'actions' => [ ['label' => 'รับเคสสอบสวนโรค', 'style' => 'SUCCESS', 'action_id' => 'accept_epi_089', 'emoji' => '🩺'] ] ]); }

10. ระบบคิวส่งซ้ำ Webhook Callback & Dead-Letter Queue (DLQ Guarantee)

เมื่อเจ้าหน้าที่คลิกปุ่มใน Discord ระบบเกตเวย์จะส่ง Webhook Callback พร้อมลายเซ็นดิจิทัล HMAC-SHA256 กลับไปยังระบบลูกทันที แต่หากเซิร์ฟเวอร์ระบบลูกขัดข้อง (เช่น HTTP 500, 502, 504 หรือ Timeout) เกตเวย์จะรับประกันความปลอดภัยของข้อมูลด้วยระบบ Retry Queue & DLQ:

Exponential Backoff Retries (5 รอบ)

ระบบจะส่งซ้ำอัตโนมัติตามช่วงเวลา: 1 นาที, 5 นาที, 15 นาที, 30 นาที และ 60 นาที โดยเก็บข้อมูลคิวลงดิสก์อย่างปลอดภัย แม้เซิร์ฟเวอร์เกตเวย์รีสตาร์ตคิวก็ไม่สูญหาย

Dead-Letter Queue (DLQ) & Manual Replay

หากส่งครบ 5 ครั้งแล้วยังไม่สำเร็จ ข้อมูลจะถูกย้ายเข้าสู่ DLQ เพื่อให้ผู้ดูแลระบบตรวจสอบผ่านหน้า Admin Portal (/admin.html) และกดปุ่ม "ส่งซ้ำทันที (Replay Now)" ได้ด้วยตนเอง

11. การแนบไฟล์เอกสาร PDF และภาพถ่ายผ่าน API (`attachments`)

รองรับการแนบไฟล์เอกสาร PDF (เช่น แบบสอบสวนโรคเบื้องต้น) หรือภาพถ่ายรอยโรค/ผื่น/ผลตรวจแล็บ ส่งตรงเข้าสู่ Discord DM โดยอัตโนมัติ:

ตัวอย่าง JSON Payload การแนบไฟล์:
{ "target_type": "user", "target_citizen_id": "1949900123456", "content": "เอกสารรายงานสอบสวนโรคระบาดเบื้องต้นประกอบเคส #EPI-2026-089", "embed": { "title": "📋 รายงานสรุปผลการสอบสวนโรค (แนบไฟล์ PDF)", "description": "กรุณาเปิดอ่านเอกสารแนบท้ายข้อความนี้", "color": "#3B82F6" }, "attachments": [ { "filename": "investigation_report_089.pdf", "url": "https://epitrack.mywellnetptn.com/downloads/report_089.pdf" }, { "filename": "rash_photo.jpg", "data": "data:image/jpeg;base64,/9j/4AAQSkZJRg...", "contentType": "image/jpeg" } ] }

12. ระบบป้องกัน Discord Rate Limiting & Outbound Pacing Engine

เพื่อป้องกันปัญหา HTTP 429 (Too Many Requests) หรือการถูกระงับ Bot Token ชั่วคราว เมื่อระบบลูกส่งข้อความกระจายตัวพร้อมกันจำนวนมาก ระบบเกตเวย์มี Token-based Serial Queue & Auto-Pacer ในตัว:

Auto-Pacing (80ms spacing)

เว้นระยะการส่งระหว่างข้อความขั้นต่ำ 80ms ต่อโทเค็นโดยอัตโนมัติ เพื่อให้เป็นไปตามข้อกำหนดความปลอดภัยของ Discord API

Auto-Retry on 429 Backoff

หากพบ HTTP 429 ระบบจะอ่าน Retry-After และหน่วงเวลารอให้อัตโนมัติ พร้อมส่งซ้ำสูงสุด 3 ครั้งโดยไม่เกิดข้อผิดพลาดกับระบบลูก

13. มาตรฐานความปลอดภัย HMAC-SHA256 Webhook Verification

เมื่อเจ้าหน้าที่คลิกปุ่มหรือส่งแบบฟอร์ม เกตเวย์จะยิง Callback กลับไปยัง webhook_url ของระบบลูก พร้อม Headers ตรวจสอบความถูกต้อง:

ตัวอย่างโค้ดตรวจสอบลายเซ็นใน PHP (Laravel / Native):
$rawPayload = file_get_contents('php://input'); $signatureHeader = $_SERVER['HTTP_X_SIGNATURE_SHA256'] ?? ''; $timestampHeader = $_SERVER['HTTP_X_SIGNATURE_TIMESTAMP'] ?? ''; $secret = getenv('GATEWAY_CLIENT_SECRET'); // ป้องกัน Replay Attack (5 นาที) if (!empty($timestampHeader) && abs(time() - (int)$timestampHeader) > 300) { http_response_code(403); die(json_encode(['error' => 'Timestamp expired'])); } $cleanSig = preg_replace('/^sha256=/', '', trim($signatureHeader)); $expected = hash_hmac('sha256', $timestampHeader . '.' . $rawPayload, $secret); if (!hash_equals($cleanSig, $expected) && !hash_equals($cleanSig, hash_hmac('sha256', $rawPayload, $secret))) { http_response_code(403); die(json_encode(['error' => 'Invalid signature'])); } // ลายเซ็นถูกต้อง ดำเนินการต่อ...

14. ระบบประวัติและการตรวจสอบย้อนหลัง Audit Trail & Activity Log (PDPA Compliant)

บันทึกข้อมูลการปฏิบัติงานเพื่อเป็นหลักฐานทางราชการ โดยซ่อนเลขบัตรประชาชน (Data Masking) ตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล:

15. ระบบตรวจสอบสถานะเซิร์ฟเวอร์ Healthcheck & Prometheus Metrics

รองรับการมอนิเตอร์ระดับ Production ผ่าน Uptime Kuma, Zabbix, Grafana และ Prometheus:

GET /health (JSON Healthcheck)

รายงานสถานะ Gateway (UP / DEGRADED), Uptime, จำนวนบอทที่ออนไลน์, จำนวนคิว Webhook และสถานะ Outbound Pacer

GET /metrics (Prometheus Exporter)

ส่งออกข้อมูลมาตรฐาน OpenMetrics เช่น bot_portal_uptime_seconds, bot_portal_cluster_bots_total, bot_portal_rate_limit_hits_total

16. ข้อควรระวังผลกระทบร้ายแรงจากการ Hardcode localhost:300... และมาตรฐานการตั้งค่า .env

การระบุ URL แบบฝังตายตัว (Hardcoded URL) เช่น http://localhost:3000 หรือ http://localhost:3001 ในซอร์สโค้ดของระบบลูก เป็นข้อผิดพลาดระดับวิกฤต (Critical Anti-Pattern) ที่ทำให้ระบบไม่สามารถทำงานได้เมื่อขึ้นโฮสต์จริง (Production Hosting / VPS)

ผลกระทบ 6 ประการเมื่อมี localhost ค้างอยู่ในโค้ดบนระบบจริง:
  • Client-Side Fetch ล้มเหลว 100%: เมื่อเจ้าหน้าที่เปิดเว็บจากภายนอก เบราว์เซอร์จะพยายามส่งคำขอไปหา localhost ของเครื่องเจ้าหน้าที่เอง เกิด ERR_CONNECTION_REFUSED หน้าจอขาวทันที
  • ถูกเบราว์เซอร์บล็อกตามกฎ Mixed Content: เว็บจริงเป็น https:// แต่โค้ดเรียก http://localhost บราวเซอร์ความปลอดภัยสูงจะบล็อกคำขอทันที
  • CORS & Content Security Policy (CSP) ปฏิเสธการเข้าถึง: นโยบายความปลอดภัยบนโฮสต์จริงจะตัดการเชื่อมต่อไปยัง localhost ทั้งหมด
  • Single Sign-On (SSO) พัง: การ Redirect หลังยืนยันตัวตนจะพาผู้ใช้กลับมาที่ localhost ของเครื่องตัวเอง ทำให้ล็อกอินไม่สำเร็จ
  • Webhook 2-Way Callback ส่งกลับไม่ถึง: แม่ข่ายบอทไม่สามารถส่งผลการกดปุ่มกลับไปยัง localhost ของระบบลูกได้
  • ไม่รองรับ Docker & Multi-Server: เมื่อแยกโฮสต์ Web Server กับ Gateway ออกจากกัน จะไม่สามารถสื่อสารข้ามเครื่องได้

แนวทางแก้ไขที่ถูกต้อง: บังคับใช้ Environment Variables (.env) 100%

ห้ามมี http://localhost:... ในไฟล์โค้ดใดๆ แม้แต่เป็นค่า fallback ตัวแปร ให้ดึงจาก process.env หรือ getenv() เท่านั้น:

# .env สำหรับระบบลูก (Production)
DISCORD_GATEWAY_URL="https://bot.mywellnet.com"
NEXT_PUBLIC_APP_URL="https://strat.pattani.go.th"
NEXT_PUBLIC_OAUTH_REDIRECT_URI="https://strat.pattani.go.th/api/auth/callback"
DISCORD_GATEWAY_WEBHOOK_SECRET="sec_xxxxxxxxxxxxxxxxxxxx"

17. สถาปัตยกรรมการจัดเก็บ Interactive Webhook Callbacks ลงฐานข้อมูลจริง (MySQL / Prisma)

การบันทึกประวัติการกดปุ่มตอบกลับของเจ้าหน้าที่จาก Discord (เช่น อนุมัติ/ไม่อนุมัติโครงการ) ต้องบันทึกลงฐานข้อมูลจริงแบบถาวร (Persistent Storage) ห้ามพึ่งพาไฟล์ชั่วคราว JSON ในโฟลเดอร์ทดสอบ เพื่อป้องกันข้อมูลสูญหายและให้สอดคล้องกับระเบียบ พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล (PDPA)

ข้อเสียของการเก็บลงไฟล์ JSON ชั่วคราว

• ข้อมูลสูญหายทันทีเมื่อเซิร์ฟเวอร์รีสตาร์ตหรือ Deploy ใหม่
• เกิด Race Conditions และ File Corruption เมื่อมีผู้ใช้กดปุ่มพร้อมกัน
• ระบบคลัสเตอร์ (PM2 / Load Balancer) แต่ละโหนดจะไม่เห็นไฟล์ของกันและกัน
• ขาด Audit Trail ทางราชการ ไม่สามารถสืบค้นย้อนหลังได้จริง

ข้อดีของ Hybrid Persistence (MySQL + Cache)

• บันทึกลงตารางจริง discord_callback_logs ข้อมูลไม่มีวันสูญหาย
• รองรับ Concurrent Users สูงผ่าน Database Transactions
• แคช 30 รายการล่าสุดใน Memory เพื่อให้หน้าบ้านแสดงผล Real-time Zero Latency
• มี Index บน citizen_id, tracking_id และ created_at ค้นหาได้ทันที

โครงสร้าง Prisma Schema (MySQL DDL)

model DiscordCallbackLog {
  id              Int      @id @default(autoincrement())
  trackingId      String?  @map("tracking_id")
  actionId        String   @map("action_id")
  event           String   @default("interaction.action")
  interactionType String   @default("BUTTON") @map("interaction_type")
  discordId       String?  @map("discord_id")
  discordTag      String?  @map("discord_tag")
  citizenId       String?  @map("citizen_id")
  fullName        String?  @map("full_name")
  departmentName  String?  @map("department_name")
  formData        String?  @db.Text @map("form_data")
  resultMessage   String?  @db.Text @map("result_message")
  createdAt       DateTime @default(now()) @map("created_at")

  @@index([citizenId])
  @@index([trackingId])
  @@index([createdAt])
  @@map("discord_callback_logs")
}