เปลือย ER Sick Level Workflow

สรุปขั้นตอนการทำงานของ ระบบรายงานประจำวันแสดงยอดผู้ป่วยห้องฉุกเฉิน โดยแบ่งตามสี และอาการเจ็บป่วย ซึ่งระบบนี้ถ้านับถอยจากวันนี้ (31 ส.ค.63) ไปก็เกิน 2 ปีแล้วที่ส่ง daily report ให้กับผู้ใช้ โดยวัตถุประสงค์คือ ต้องการทราบยอดผู้ป่วย ต่อวันที่มาเข้ารับบริการที่ห้องฉุกเฉิน โดยแบ่งตามเฉดสี ขาว เขียว เหลือง ชมพู แดง ต่อมาแยกผู้ป่วยที่มีอาการบาดเจ็บ และไม่มีด้วย โดยล่าสุดได้ปรับ function การทำงานให้กระชับที่สุด จากที่เคยต้องใช้ host ภายนอก โยนไฟล์กันข้ามไปมา ตั้ง cronjob ไว้ 2 ฝั่ง เพื่อแบ่งการทำงาน ล่าสุดเอามารวมกันเพื่อไม่ให้ยากต่อการ maintenance

System Requirement

  • SQL Server : เป็นฐานข้อมูลของระบบสารสนเทศโรงพยาบาล (HIS)
  • Nodejs-app : เป็น API เชื่อมกับ HIS
  • PHP: สร้างภาพ และ สร้าง flex message ส่งเข้า LINE Messaging API
  • LINE DEV: ใช้เพื่อ push ข้อความส่งเข้าไปในกลุ่ม เป็นรูปแบบ flex message
  • Cronjob: ตั้งเวลาเพื่อให้ระบบทำงานอัตโนมัติ
  • etc. อื่น ๆ

SQL Execute Stored Procedure

ตอนเริ่มต้น project นี้ ทำแค่ VIEW สำหรับแสดงยอดรวม ต่อมามี requirement เพิ่มขึ้นเรื่อยๆ ทำให้ VIEW ต้องแตกไปอีกจนรู้สึกว่ามันเละเทะเกินไป จากเดิมมี ER_SICK_LEVEL_YESTERDAY เพื่อแสดงข้อมูลสรุปของเมื่อวาน ก็แตกมาเป็น _YESTERDAY_MORNING เวรเช้า, _YESTERDAY_AFTERNOON เวรบ่าย และ _YESTERDAY_NIGHT เวรดึก เพื่อแสดงข้อมูลแต่ละช่วงเวลา ซึ่งในเวอร์ชั่นนี้ ได้ใช้ flex message แล้วเนื่องจากสามารถส่งเป็น carousel ได้ดังภาพ

ยกตัวอย่าง SQL VIEW ที่เริ่มต้นระบบสำหรับสรุปยอดแบบแยกตามช่วงเวลา โดยแก้ไข E.visitTime ให้เป็นช่วงเวลาที่ต้องการ

SELECT
  Sick_Level,
  SUM(CASE WHEN trauma='Y' THEN 1 ELSE 0 END) as TRAUMA_YES,
  SUM(CASE WHEN trauma='N' THEN 1 ELSE 0 END) as TRAUMA_NO,
  SUM(CASE WHEN trauma='0' THEN 1 ELSE 0 END) as TRAUMA_NULL,
  COUNT(Sick_Level) AS level_count
FROM
(
  SELECT 
    Sick_Level,
    ER_CASE
FROM
  ExampleTable E
WHERE
  -- คนไข้แผนกฉุกเฉิน
  E.clinic = 'ER'
  -- ที่มารับบริการเมื่อวาน
  AND E.visitDate = CAST(GETDATE() - 1 AS DATE)
  -- แบ่งตามช่วงเวลา เช้า, บ่าย, ดึก
  AND E.visitTime BETWEEN '00:00:00' AND '08:29:00'
) O
GROUP BY
  GROUPING SETS ((O.Sick_Level),());

ปัจจุบันได้เปลี่ยนมาเป็น Stored Procedure เนื่องจาก ต้องการเพิ่มคำสั่ง หรือ query เข้าไปใน SQL Statement ด้วยวิธีการใส่ช่วงเวลาเข้าไป code เพื่อรับค่าเป็น SQL Statement เข้าไปเลย (วิธีนี้มีความเสี่ยงโปรดใช้อย่างระมัดระวัง)

CREATE PROCEDURE dbo._MOREMENG_ER_SICKLEVEL
    @cause NVARCHAR(MAX) = ''
AS
BEGIN
    DECLARE @SQL NVARCHAR(MAX)

    SELECT @SQL = '
      SELECT
        Sick_Level,
        SUM(CASE WHEN trauma=''Y'' THEN 1 ELSE 0 END) as TRAUMA_YES,
        SUM(CASE WHEN trauma=''N'' THEN 1 ELSE 0 END) as TRAUMA_NO,
        SUM(CASE WHEN trauma=''0'' THEN 1 ELSE 0 END) as TRAUMA_NULL,
        COUNT(Sick_Level) AS level_count
      FROM
      (
        SELECT 
          Sick_Level,
          ER_CASE
      FROM
        ExampleTable E
      WHERE
        -- คนไข้แผนกฉุกเฉิน
        E.clinic = ''ER''
        -- ที่มารับบริการเมื่อวาน
        -- แบ่งตามช่วงเวลา เช้า, บ่าย, ดึก
        ' + @cause + '
    ) O
    GROUP BY
      GROUPING SETS ((O.Sick_Level),());';

    -- PRINT @SQL
    EXEC sys.sp_executesql @SQL

    RETURN 0
END

วิธีการเรียกใช้โดยใช้คำสั่ง EXEC Procedure_name 'parameter' อ่านรายละเอียดเพิ่มเติม

NodeJS + Express + MSSQL

เนื่องจากฐานข้อมูลเป็น Microsoft SQL Server ทำให้การใช้ PHP driver ไม่ค่อยจะดีนัก (เคยเขียนวิธีการเชื่อมต่อไว้ การติดตั้ง PHP connect to SQL Server) เพราะไม่ใช่ทางของมันถ้าเทียบกับ .NET, C# แล้วค่อนข้างยุ่งยากกว่า และการจำลอง environment ก็จะทำให้มีปัญหากับเครื่องที่ใช้พัฒนาด้วย เพราะต้องติดตั้ง .net framework, MS visual c++ หลากหลายเวอร์ชั่น กว่าจะเข้าที่เข้าทาง ซ้ำร้ายบนเครื่อง production จริง ๆ ก็เป็น LINUX ทำให้คุยกันยากเข้าไปอีก ดังนั้น Nodejs เลยเป็นท่าที่น่าจะจบสวย ซึ่งไฟล์ package.json ก็มีเท่านี้

{
  "name": "nodejs-app",
  "version": "1.0.0",
  "description": "ER Quere board API with Express and mssql",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon --watch server.js"
  },
  "author": "MoreMeng",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "morgan": "^1.10.0",
    "mssql": "^5.1.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.4"
  }
}

เหตุผลที่บอกว่าจบสวยเป็นเพราะว่า เราทำเป็น API ไว้ ทำให้เราดัก Error มันไว้ได้ ไม่ไปเอ๋อบน production แน่นอน มาดูตัวอย่าง code ในส่วนของ routes และ controller กัน มีแค่นี้

module.exports = app => {
    const routes = require(`../controllers/emergency.controller.js`)

    app.get('/er-table-daily', routes.erDailyQuery)
    app.get('/er-table-night', routes.erNightQuery)
    app.get('/er-table-morning', routes.erMorningQuery)
    app.get('/er-table-afternoon', routes.erAfternoonQuery)
    app.get('/er-table-monthly/:month', routes.erMonthlyQuery)
}
const Query = require("../models/query.model")

exports.erDailyQuery = (req, res) => {
    let sqlstm = `EXEC _MOREMENG_ER_SICKLEVEL "AND E.registDate = CAST(GETDATE()-1 AS DATE) AND E.visitDate = CAST(GETDATE()-1 AS DATE)";`
    Query.executeQuery(sqlstm, res)
}
exports.erAfternoonQuery = (req, res) => {
    let sqlstm = `EXEC _MOREMENG_ER_SICKLEVEL 'AND E.registDate = CAST(GETDATE()-1 AS DATE) AND E.visitDate = CAST(GETDATE() - 1 AS DATE) AND E.visitTime BETWEEN ''16:30:00'' AND ''23:59:00''';`
    Query.executeQuery(sqlstm, res)
}
exports.erMorningQuery = (req, res) => {
    let sqlstm = `EXEC _MOREMENG_ER_SICKLEVEL 'AND E.registDate = CAST(GETDATE()-1 AS DATE) AND E.visitDate = CAST(GETDATE() - 1 AS DATE) AND E.visitTime BETWEEN ''08:30:00'' AND ''16:29:00''';`
    Query.executeQuery(sqlstm, res)
}
exports.erNightQuery = (req, res) => {
    let sqlstm = `EXEC _MOREMENG_ER_SICKLEVEL 'AND E.registDate = CAST(GETDATE()-1 AS DATE) AND E.visitDate = CAST(GETDATE() - 1 AS DATE) AND E.visitTime BETWEEN ''00:00:00'' AND ''08:29:00''';`
    Query.executeQuery(sqlstm, res)
}
exports.erMonthlyQuery = (req, res) => {
    let sqlstm = `EXEC _MOREMENG_ER_SICKLEVEL 'AND LEFT(E.registDate,6) = ''${req.params.month}''';`
    Query.executeQuery(sqlstm, res)
}

PHP – Create Images

ใช้ความสามารถของ PHP Image Processing and Generation เพื่อสร้างภาพสวย ๆ ออกมา วิธีได้เคยทำสมัยหัดเล่น API ของ Twitter ใหม่ โดยดึง tweet มาใส่ในภาพและเปลี่ยนพื้นหลังตามช่วงเวลา (เอาไปโชว์ไว้บน Hi5 ด้วยนะเออ) โดยส่วนประกอบของขั้นตอนนี้จะมีอยู่ 3 ส่วน

  • Background Image: ภาพพื้นหลัง ที่ยากต่อการใช้ GD สร้าง
  • DATA: ข้อมูลที่จะนำมาสร้าง ส่วนใช้ใช้ json เนื่องจากเอาไปใช้ง่ายและสะดวกกว่า
  • Enabled PHP GD: ถ้าไม่เปิดใช้งานมันจะสร้างภาพไม่ได้

Background image

ทำเพื่อให้เป็นกรอบในการแสดงข้อมูล เนื่องจากบางชิ้นส่วนนั้นใช้ GD สร้างไม่ได้ หรือ วุ่นวายต่อการสร้าง และเป็นส่วนที่ไม่จำเป็นต้องสร้าง ก็ให้ใช้ภาพพื้นหลังแทน

DATA

ข้อมูลได้มาจากข้างต้น โดยการเรียก API ด้วยวิธีธรรมดาอย่าง file_get_contents โดยตัวอย่างการดึงข้อมูลมีข้อจำกัดเรื่อง ผลรวม ซึ่งหากได้ข้อมูลที่มันเอามาใช้งานได้เลย มันก็ย่อมดีกว่าต้องมาเขียน summary ใน PHP ซึ่งอาจจะได้ยอดที่ไม่ตรงเนื่องจากการตั้งเงื่อนไขผิดพลาดก็เป็นได้ เลยใช้วิธีการ GET API เป็นรอบ ๆ จำนวน 5 รอบด้วยกัน ดังตัวอย่างนี้

กำหนด server:port ของ nodejs-app ที่เชื่อมกับ MSSQL เอาไว้

// local
$server = 'http://127.0.0.1:7777/';

กำหนด array ช่วงเวลา ในส่วนนี้ก็จะเป็น routes นี่เอง และหากวันที่ ตรงกับวันที่ 1 ก็ให้แสดง er-table-monthly/{month} ด้วย

$time = [
    'daily'     => 'er-table-daily',
    'night'     => 'er-table-night',
    'morning'   => 'er-table-morning',
    'afternoon' => 'er-table-afternoon'
];
if ( date( 'd' ) == 1 ) {
    $time['monthly'] = 'er-table-monthly/' . $date_monthly;
}

จากนั้นก็ foreach เพื่อวนลูปดึงข้อมูล และ สร้างภาพ

foreach ( $time as $key => $value ) {
    $data = file_get_contents( $server . $value );
    SaveImage( json_decode( $data ), $key );
}

Create Image

เมื่อมีข้อมูลแล้ว มีภาพพื้นหลังแล้ว ขั้นตอนการสร้างภาพจะใช้ความสามารถของ PHP gd ในการสร้างข้อความ หรือ กราฟ เข้าไปในภาพ โดย code ก็จะมี 2 ส่วน

  • Properties: ขั้นตอนการตั้งค่า สี ความสูงของบรรทัด ตำแหน่งการจัดวาง ที่จัดเก็บภาพ
  • Generation: ขั้นตอนการสร้างภาพ จากค่าที่ถูกกำหนด และจากข้อมูลที่ได้มา

กำหนดรูปแบบ font

php gd เราสามารถใช้ font ที่เป็น true type ได้ พวกไฟล์ที่นามสกุล .ttf ทั้งหลายนั้นเอง ซึ่งข้อความที่เราสร้างก็จะออกมาหน้าตาเหมือนกับ font ที่เราเรียกใช้ทุกประการ

$font      = dirname( __FILE__ ) . '/ttf/THA0101.ttf';
$font2     = dirname( __FILE__ ) . '/ttf/DS-DIGIB.TTF';
$font3     = dirname( __FILE__ ) . '/ttf/CSChatThaiUI.ttf';
$font_size = 20;

กำหนดค่าสี และระยะ

โดยใช้ function imagecolorallocate() เพื่อกำหนดค่าสี RGB และ imagecolorallocatealpha() สำหรับ RGBA คือสีที่กำหนด transparent ไว้ด้วย

    $start_y     = 200;
    $line_height = 55;

    $im = CreateImage( $output );
        // Create some colors
    $white = imagecolorallocate( $im, 255, 255, 255 );
    // $gray = imagecolorallocate($im, 100, 100, 100);
    $black = imagecolorallocate( $im, 0, 0, 0 );
    $red   = imagecolorallocate( $im, 240, 0, 0 );

    $level_color = [
        0       => imagecolorallocate( $im, 0, 0, 0 ),
        1       => imagecolorallocate( $im, 249, 105, 116 ),
        2       => imagecolorallocate( $im, 249, 140, 226 ),
        3       => imagecolorallocate( $im, 249, 230, 140 ),
        4       => imagecolorallocate( $im, 153, 249, 140 ),
        5       => imagecolorallocate( $im, 255, 255, 255 ),
        'Total' => imagecolorallocate( $im, 0, 0, 0 )
    ];
    // $brown = imagecolorallocate($im, 120, 100, 90);
    $shadow = imagecolorallocate( $im, 45, 45, 45 );
    $alpha  = imagecolorallocatealpha( $im, 255, 255, 255, 50 );

สร้างภาพ และ ข้อความ

ในกระบวนการสร้างภาพนี้จะใช้ imagettftext เพื่อสร้างข้อความลงบนภาพ โดยกำหนดสีและขนาดไว้แล้ว ซึ่งการวนลูปก็ต้องขยับ ให้ข้อความขึ้นบรรทัดใหม่ด้วยวิธีการ “ตำแหน่่ง = (ความสูงของช่อง x รอบ) + จุดเริ่มต้น” จะได้ $axisY คือ ตำแหน่งเริ่มต้นของ รอบถัดไป

$axisY = ( $line_height * $levelNumber ) + $start_y;

ส่วนวิธีการเติม text shadow ให้กับข้อความก็ใช้ imagettftext() 2 ครั้ง ครั้งแรก สีเข้ม ครั้งที่ 2 สีปกติ ซึ่งการทำงานของมันจะทับของเดิม เราไม่สามารถเติมสีไปด้านหลังได้ (ให้คิดถึงตอนลงสีจริงๆ) โดยให้ x , y ของสีพื้น มีค่าน้อยกว่าสีหลัง 1 pixel เช่น สีดำพื้นหลัง x=51, y=580 ดังนั้นสีขาวจะต้อง x=50, y=579

function SaveImage( $array, $output ) {
    global $now, $font, $font2, $font3, $font_size, $date_name;

    $start_y     = 200;
    $line_height = 55;

    $im = CreateImage( $output );

    // Create some colors
    $white = imagecolorallocate( $im, 255, 255, 255 );
    // $gray = imagecolorallocate($im, 100, 100, 100);
    $black = imagecolorallocate( $im, 0, 0, 0 );
    $red   = imagecolorallocate( $im, 240, 0, 0 );

    $level_color = [
        0       => imagecolorallocate( $im, 0, 0, 0 ),
        1       => imagecolorallocate( $im, 249, 105, 116 ),
        2       => imagecolorallocate( $im, 249, 140, 226 ),
        3       => imagecolorallocate( $im, 249, 230, 140 ),
        4       => imagecolorallocate( $im, 153, 249, 140 ),
        5       => imagecolorallocate( $im, 255, 255, 255 ),
        'Total' => imagecolorallocate( $im, 0, 0, 0 )
    ];
    // $brown = imagecolorallocate($im, 120, 100, 90);
    $shadow = imagecolorallocate( $im, 45, 45, 45 );
    $alpha  = imagecolorallocatealpha( $im, 255, 255, 255, 50 );

    foreach ( $array->recordset as $row ) {

        $sickLevel = ( isset( $row->Sick_Level ) ) ? $row->Sick_Level : 'Total';

        $levelNumber = ( isset( $row->Sick_Level ) ) ? $row->Sick_Level : 6;

        $axisY = ( $line_height * $levelNumber ) + $start_y;

        // imagettftext($im, $font_size, 0, 70,  $axisY, $level_color[$sickLevel], $font3, $axisY );

        imagettftext( $im, $font_size, 0, 160, $axisY, $level_color[$sickLevel], $font3, $row->ER_TYPE_YES );
        imagettftext( $im, $font_size, 0, 270, $axisY, $level_color[$sickLevel], $font3, $row->ER_TYPE_NO );
        imagettftext( $im, $font_size, 0, 380, $axisY, $level_color[$sickLevel], $font3, $row->ER_TYPE_NULL );
        imagettftext( $im, $font_size, 0, 490, $axisY, $level_color[$sickLevel], $font3, $row->level_count );

    }

    // DATE
    imagettftext( $im, 39, 0, 325, 62, $shadow, $font2, $now );
    imagettftext( $im, 39, 0, 323, 61, $shadow, $font2, $now );
    imagettftext( $im, 39, 0, 321, 60, $white, $font2, $now );

    // credit
    imagettftext( $im, 12, 0, 51, 580, $shadow, $font3, 'Created by @MoreMeng #ICT Angthong Hospital' );
    imagettftext( $im, 12, 0, 50, 579, $white, $font3, 'Created by @MoreMeng #ICT Angthong Hospital' );

    // OUTPUT SAVE FILE LOCATION
    imagejpeg( $im, dirname( __FILE__ ) . '/tmp/' . $date_name . $output . '.jpg', 90 );
}

CreateImage function นี้ถูกแยกออกมาเนื่องจากต้องการทดสอบ กระบวนการทำหลาย ๆ แบบ โดย source file เป็น jpeg บ้าง png บ้าง เพื่อทดสอบดู performance ดูระยะเวลาในการสร้างภาพ และ คุณภาพของไฟล์ที่ได้ ขนาดของไฟล์ สุดท้ายเลือกไฟล์ background ที่เป็น png 24bit (เจอปัญหากับ png 8bit เวลาสร้างภาพแล้วสีข้อความมันไม่มา)

function CreateImage( $output ) {
    $image = dirname( __FILE__ ) . '/images/' . $output . 'bg-600x600.png';
    $im    = imagecreatefrompng( $image );

    return $im;
}

ผลลัพท์จะได้ไฟล์แบบนี้ทุกวัน โดยชื่อไฟล์จะเป็น YYYYMMDD-{times}.jpg

PHP – LINE Messenger API – FlexMessage

หลังจากได้ภาพรายวันมาแล้ว สุดท้ายก็ต้องทำตัวส่งข้อมูลไปยังผู้ใช้ โดยสร้าง LINE Channel แบบ Messaging API เพื่อให้ Bot ตัวนี้ส่งข้อความเข้าไปในกลุ่ม (วิธีสร้าง LINE Bot) หลังจากสร้าง Chatbot ขึ้นมาแล้วก็ invite เข้าไปในกลุ่ม LINE เพื่อให้พร้อมต่อการยิงข้อความแบบ push message ทุกวัน (Cronjob อยู่ในกัวข้อถัดไป) ซึ่งแรกเริ่มนั้นส่งเข้ากลุ่มใหม่ users ประมาณ 450 ก่อนหน้าที่ LINE OA จะปรับโฉมใหม่ก็ส่งกันพร่ำเพรื่อ พอถูก LIMIT ก็เลยสร้างกลุ่มใหม่ขึ้นมา ให้ผู้่รับผิดชอบเท่านั้นที่เป็นคนรับข้อมูล โดยมี 10 user จำนวนที่ส่งก็น้องลงคิดเป็น 10 ข้อความต่อวัน

ขั้นตอนในอดีต

ข้อจำกัดของการส่งภาพใน FlexMessage คือต้องเป็น https เท่านั้น!! ทำให้ local server เอาก็ต้องใช้ SSL เช่นเดียวกัน แต่เนื่องจากใช้ public IP จึงใช้่ Let’s Encrypt สร้าง SSL ไม่ได้!!! (https://letsencrypt.org/docs/faq/#what-ip-addresses-does-let-s-encrypt-use-to-validate-my-web-server) ถ้าจะรั้นใช้ IP จริง ๆ ขั้นตอนก็ค่อนข้างยุ่งยาก จึงใช้วิธีการ ดังนี้

Host A (local, IP)
Host B (inter, domain, SSL)

Step #1: DATABASE —> Host A {API} —> Host B {images}
Step #2: Host B {images} —> Host A {LINE} —> LINE Group

ภายหลังใช้วิธีการนี้ มาช่วยให้ขั้นตอนกระชับขึ้น โดยสร้าง subdomain และชี้มาที่ public ip และใช้ SSL key ร่วมกัน

ขั้นตอนในปัจจุบัน

Host A (local, IP, domain, SSL)

Step #1: DATABASE —> Host A {API, images}
Step #2: Host A {LINE,images} —> LINE Group

ตัวอย่าง code สำหรับการส่ง

  • $receiveId คือ LINE userId หรือ groupId ในที่นี้ใช้วนลูปเพื่อสร้างหลาย ๆ channel พร้อมกัน
  • $fix_cached คือ parameter หลัง image url เพื่อป้องกันเรื่อง cached ใน LINE เวลาที่เกิดข้อผิดพลาดแล้วส่งใหม่ จะได้ ไม่ซ้ำภาพเดิม
  • $monthly คือ bubble ของรายเดือน หากวันที่ตรงกับวันที่ 1 ก็จะมีสรุปยอดของเดือนที่แล้วให้ด้วย
  • $contents คือ flex message ทั้งก้อน
error_reporting( E_ERROR | E_WARNING | E_PARSE );

require_once dirname( __FILE__ ) . '/../config.php';
require_once DEV_PATH . '/functions/global.php';

define( 'ACCESS_TOKEN', 'channel access token' );

$receiveId = [
    'user id'  => 'Thanikul Sriuthis',
    'group id' => 'ATH EMS'
];

$yesterday      = thai_date( date( 'Y-m-d', strtotime( "-1 days" ) ), 0, 0 );
$file_yesterday = date( 'Ymd-', strtotime( "-1 days" ) );

// Fixed LINE Cached image
$fix_cached = base64_encode( date( 'YmdHis' ) );

$https_files  = 'https://example.com/infographic/tmp/';
$link_image   = '';
$line_liffapp = 'line://app/9999999999-mdYeYvnB';
$line_desktop = 'https://example.com/liff/photos';
$logo_url     = 'https://example.com/infographic/images/ATH-LOGO-3-300x300.png';
$monthly      = '';

$curl = curl_init();

// monthly report if date = 1
if ( date( 'd' ) == 1 ) {
    $monthly = '{ "type": "bubble", "styles": { .... } },';
}

$contents = '{ "type": "carousel", "contents": [' . $monthly . '{ "type": "bubble", "styles": { .... } }]}';

foreach ( $receiveId as $key => $name ) {

    curl_setopt_array( $curl, [
        CURLOPT_URL            => "https://api.line.me/v2/bot/message/push",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING       => "",
        CURLOPT_MAXREDIRS      => 10,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST  => "POST",
        CURLOPT_POSTFIELDS     => '{ "to": "' . $key . '", "messages": [ { "type": "flex", "altText": "สรุปยอดผู้ป่วยใช้บริการห้องอุบัติเหตุและฉุกเฉิน ' . $yesterday . '",
"contents": ' . $contents . ' } ]}',
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer " . ACCESS_TOKEN,
            "Content-Type: application/json"
        ]
    ] );

    $response = curl_exec( $curl );
    $err      = curl_error( $curl );

    if ( $err ) {
        echo "cURL Error #:" . $err;
    } else {
        echo $response;
    }
}

curl_close( $curl );

Cronjob – Daily Report

ใครจะขยันมากดส่งได้ทุกวัน เมื่อความขี้เกียจก่อเกิดนวัตกรรม จึงจำเป็นต้องมีตัวช่วย อย่าง Cronjob โดยแยกเป็น 2 service คือ สร้างภาพ และ นำส่งข้อมูล

สร้างภาพโดยไปเรียก PHP – monthly-er-sick-level.php เวลา 04.00 ทุกวัน

0 4 * * * /usr/bin/php -q /home/path/to/monthly-er-sick-level.php #create images

ส่งข้อความโดยไปเรียก PHP – er-daily-flex.php เวลา 05.00 ทุกวัน

0 5 * * * /usr/bin/php -q /home/path/to/er-daily-flex.php #ER Daily Report

การกำหนดค่าใน cronjob จะอ้างอิงตามนี้

# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name  command to be executed

สรุป

กระบวนการทำงานเดิมที่ค่อนข้างซับซ้อนวุ่นวาย พอทบทวนใหม่ ปรับ code ใหม่ clean data ใหม่ ทำให้ดูง่ายขึ้น ซึ่งอนาคตก็ยังไม่แน่ว่าอาจจะปรับเป็น nodejs ทั้งหมด (ถ้าเป็นไปได้) เนื่องจากลดความยุ่งยากลง การมี service เยอะ ๆ ก็ไม่ใช่เรื่องดี แต่ข้อจำกัดหลายอย่างทำให้ต้องใช้ platform ที่ค่อนข้างหลากหลาย และปัญหาก็ไม่ได้มีเรื่องของ source code เสมอไป เนื่องจากมีการใช้ Service จากภายนอก เช่น LINE Messaging API สิ่งที่ต้องระลึกไว้เสมอว่า เขาอาจจะเปลี่ยนวันไหนก็ได้ โดยที่เราอาจจะไม่ทันตั้งตัว หรือ มีมาตรการรองรับ core api อาจจะมีการเปลี่ยนเวอร์ชั่น เปลี่ยนกระบวนการทำงาน ซึ่งทั้งหมดก็ต้องดูผลกระทบกับระบบ และปรับปรุงแก้ไขโดยไม่รู้จบสิ้น

ของเดิม
ของปัจจุบัน

Cover Image: Banner vector created by upklyak – www.freepik.com