Tech Blog

สร้าง E-Scooter Sharing ระดับท้องถนนตั้งแต่ศูนย์เพื่อทำความเข้าใจ — ชุดบันทึก Design, Implementation, และ Operations

by Tech Writer
Flutter Spring Boot Java PostgreSQL Mobility IoT Design

บทนำ

ในช่วงไม่กี่ปีที่ผ่านมา พอร์ตสำหรับ e-scooter sharing (เช่าสกู๊ตเตอร์ไฟฟ้า) ปรากฏที่สถานีรถไฟ ที่จอดรถของร้านสะดวกซื้อ และข้างทางในเมือง การกดแอปเพื่อปลดล็อก ขับ และคืนที่พอร์ตปลายทาง — ในมุมผู้ใช้แล้ว ประสบการณ์เรียบง่ายมาก

แต่ในมุมวิศวกร flow ที่ดูเรียบง่ายมักก่อคำถามตามมา:

  • ตัวเลข “สกู๊ตเตอร์ว่าง” และ “ช่องว่าง” บนแผนที่ถูกคำนวณแบบ near real-time ได้อย่างไร
  • ทำไมการกดปุ่มในแอปจึงทำให้ กุญแจกลไกจริง บนสกู๊ตเตอร์ที่ข้างถนนปลดล็อกได้
  • บนเซิร์ฟเวอร์ที่ไหน และอ้างอิงข้อมูลใด ที่ตัดสินว่า “พอร์ตนี้รับคืนรถไม่ได้”
  • หากผู้ใช้พยายามปลดล็อกรถในเสี้ยววินาทีเดียวกับที่ swapper พยายามถอดแบตเตอรี่และ admin พยายามสั่งปิดระยะไกล จะแก้ contention อย่างไร

สิ่งเหล่านี้มองจากภายนอกไม่ออก

ชุดบทความนี้คือบันทึกของ การพยายามเข้าใจทุกอย่างนั้นด้วยการเก็งร่าง ออกแบบ และ implement จากศูนย์ — ในรูปแบบโปรเจกต์ส่วนตัว บทความไม่ใช่การโชว์ผลิตภัณฑ์สำเร็จ แต่เป็นบันทึกการลองผิดลองถูก: “ถ้าฉันต้องสร้างบริการระดับท้องถนนนี้จากศูนย์ ฉันต้องคิดอะไรบ้าง?” — โดยจับมุมทั้งฝั่ง design และ implementation

โปรเจกต์นี้อยู่ระหว่างการพัฒนา บทความในชุดนี้ไม่ใช่รายงานความสำเร็จ แต่เป็นบันทึก design และ implementation ที่กำลังดำเนินอยู่ มีการเพิ่มบทความและบันทึก design ใหม่ตามที่ feature เติบโต


ทำไมเลือก E-Scooter Sharing เป็นหัวข้อ

มีเหตุผล 3 ข้อ

1. เป็นบริการที่ทุกคนเห็นบนถนน แต่ภายในไม่ชัดเจน

ในมุมผู้ใช้แล้วเรียบง่าย แต่ภายในครอบคลุม mobile app, REST API, database, IoT messaging, external payment, และ map service — ภาพตัดขวางที่ครบของสิ่งที่ปรากฏในระบบสมัยใหม่ หัวข้อเดียวให้ฝึก mobile UI, server-side logic, schema design, external integration, และ real-time communication ขนานกัน

2. ผู้ใช้หลายฝ่ายแย่งใช้ข้อมูลเดียวกัน

ผู้ใช้ (คนขับ), swapper (เปลี่ยนแบตเตอรี่), และ admin (มอนิเตอร์ระยะไกล) ทั้งสามแตะ resource “สกู๊ตเตอร์” เดียวกันด้วยจุดประสงค์การปฏิบัติงานต่างกัน State machine และ concurrency จึงกลายเป็นเรื่องที่ต้องจัดการจริง ไม่ใช่แค่ตัวอย่างในตำรา

3. เป็นบททดสอบของรูปแบบ “design-while-building”

ฉันอยากทดสอบว่า flow แบบคลาสสิก Requirements → Architectural → Detailed → API design → Implementation จะ รันบนโปรเจกต์ส่วนตัวคนเดียว ได้จริงไหม เมื่อคนเดียวกันเขียน design และ code คนเขียน design มักเริ่มเพิกเฉยต่อ design ของตัวเอง — และทั้งหมดพังลง การเก็บบันทึก วิธีป้องกันสิ่งนั้น จึงเป็นเป้าหมายชัดเจน


ระบบที่ตั้งใจจะสร้าง

จุดเริ่มต้นของ architecture คือผู้ใช้ 3 ฝ่าย × แอป 3 ตัว + backend + IoT

[User Tier]
  General User          Swapper                Administrator
      |                      |                      |
      v                      v                      v
+----------------+   +----------------+   +-------------------------+
| User App       |   | Swapper App    |   | Admin Console (SSR)     |
| Flutter        |   | Flutter        |   | Spring Boot Thymeleaf   |
+----------------+   +----------------+   +-------------------------+
         \                  |                  /
          \                 |                 /
           +----------------+----------------+
                            |
                            v
                +--------------------------+
                | Backend API Platform     |
                | Java 21+ Spring Boot     |
                +--------------------------+
                 |        |         |       \
                 v        v         v        v
        +----------------+ +----------------+ +----------------------+
        | AuthN / AuthZ  | | Business Logic | | IoT Gateway          |
        | FIDO2/JWT/RBAC | | Ride/Fleet/Swap| | MQTT Pub/Sub         |
        +----------------+ +----------------+ +----------------------+
                                                       |
                                                       v
                                            +----------------------+
                                            | MQTT Broker          |
                                            +----------------------+
                                                       |
                                                       v
                                            +----------------------+
                                            | On-Vehicle IoT       |
                                            | BMS/GPS/Smart Lock   |
                                            +----------------------+
ApplicationStackกลุ่มผู้ใช้
User AppFlutter (iOS/Android)ผู้ขับขี่
Swapper AppFlutter (iOS/Android)คนเปลี่ยนแบตเตอรี่ / บำรุงรักษาภาคสนาม
Admin ConsoleSpring Boot + Thymeleaf (SSR)มอนิเตอร์การดำเนินงานและควบคุมระยะไกล
Backend APIJava 21+ / Spring Boot 3.xPlatform กลางของ 3 แอป
DatabasePostgreSQL + PostGISคำนวณ polygon ของพอร์ตจำเป็นต้องมี spatial extension
IoTMQTT Broker → on-vehicle deviceคำสั่งปลดล็อกและ telemetry
External serviceFIDO2/SMS/eKYC/Stripe/maps/notificationsAuthN, KYC, payment, map

External dependency (SMS / payment / eKYC / IoT) ถูก mock ภายใต้ dev profile ก่อน integration จริง เป้าหมายคือยืนยันว่า “ทุกหน้าจอ flow ครบ end-to-end” ก่อนเชื่อมของจริง บทความหลายชิ้นในชุดนี้พูดถึงการตัดสินใจตัดทอนแบบ dev profile นี้


การเลือกเทคโนโลยี

LayerTechเหตุผล
Mobile appFlutter 3.xCodebase เดียวสำหรับ iOS/Android
Backend APIJava 21 + Spring Boot 3.xTrack record ระดับ enterprise; JPA / Security / Profile พร้อมใช้
Admin consoleSpring Boot + Thymeleafภาษาและ framework เดียวกับ backend ลด cognitive load
DatabasePostgreSQL 17 + PostGISตรวจ polygon พอร์ต, ค้นหา spatial ของสกู๊ตเตอร์
IoTMQTTLow-latency bidirectional device messaging
AuthenticationFIDO2/WebAuthn + JWTPasskey เป็นหลัก; JWT สำหรับ stateless API session
State management (mobile)RiverpodCross-screen state และ async fetch coordination
HTTP (mobile)dioเขียน interceptor ง่าย
Map renderingflutter_map (OpenStreetMap)ไม่ต้องกังวล license; เหมาะกับ development โดยไม่ต้อง setup บัญชี

อภิธานศัพท์ — Screen ID และ Operational ID

ระบุที่มาจาก design doc ปรากฏตรง ๆ ในบทความ ใช้ตารางนี้เป็นการอ้างอิงเมื่อเจอ “S02” หรือคล้าย ๆ

Screen ID (User App, S01–S13)

IDหน้าจอบทบาท
S01SplashApp startup, ตรวจ session
S02Home mapแสดงพอร์ตและสกู๊ตเตอร์ว่างใกล้เคียงบนแผนที่
S03QR scanอ่าน QR ของสกู๊ตเตอร์ที่จะใช้
S04เลือกพอร์ตปลายทางเลือกพอร์ตคืนรถจากแผนที่
S05Login / SMS authยืนยันบัญชีด้วยเบอร์โทร + OTP
S06ลงทะเบียน paymentลงทะเบียน method ชำระเงิน (เช่นบัตร)
S07ทดสอบกฎจราจรทดสอบก่อนขับตามที่กฎหมายกำหนด
S08ยื่นเอกสาร eKYCถ่ายใบขับขี่และยืนยันตัวตน
S09ยืนยันเริ่มขับยืนยันรถ / ปลายทาง / ค่าใช้จ่ายขั้นสุดท้าย; ปลดล็อก
S10ขณะขับนำทาง real-time, เวลาที่ผ่านไป, ค่าใช้จ่ายที่กำลังเดิน
S11ยืนยันการคืน + รูปถ่ายตัดสินพอร์ตคืน, รูปถ่ายหลักฐาน
S12ใบเสร็จแสดงเวลาและจำนวนเงินสุดท้าย
S13เมนูบัญชีข้อมูลผู้ใช้, ประวัติ, FAQ

Operational identifier ที่ใช้ในกระบวนการ design

แนะนำเพื่อจัดการช่องว่างระหว่าง design กับ implementation ดูเรื่องเต็มได้ที่ ไม่ให้ implementation ห่างจาก design

Identifierความหมาย
DVG-XXXDivergence ID — หนึ่งหมายเลขต่อหนึ่งความเบี่ยงเบนระหว่าง design กับ implementation
TASK-XXImplementation-diff task ID — หนึ่งหมายเลขต่อหนึ่งงานที่ปิด divergence
app-apiScope tag ที่ระบุว่าประเด็นครอบคลุมทั้งฝั่ง Flutter app และ Spring Boot API

หัวข้อที่ครอบคลุมและแผนที่บทความ

แต่ละบทความสกัด การตัดสินใจ design หรือ implementation snag เฉพาะหนึ่งจุด ที่เจอระหว่างสร้างระบบนี้ บทความถูกจำกัดขอบเขตแคบ จึงอ่านแยกชิ้นเพื่อค้นเทคนิคใดเทคนิคหนึ่งก็ได้


■ ขับเคลื่อนระบบ end-to-end ด้วยทางสั้นที่สุด

หัวข้อบทความ
Home map → ขับ → คืน → ใบเสร็จ ใน E2E ขั้นต่ำหนึ่งเส้นทำให้แอป E-Scooter Sharing วิ่งครบ E2E ขั้นต่ำ — Flutter + Spring Boot กับการตัดทอนด้วย dev profile

■ Mobile UI implementation

หัวข้อบทความ
ทำไม map library เปลี่ยนจาก Google Maps ไป OpenStreetMap และวิธีอยู่ร่วมกับ OSM tile อย่างเหมาะสมเปลี่ยน Map Library ของ Flutter จาก Google Maps ไป flutter_map (OpenStreetMap) — เกณฑ์การเลือกสำหรับโปรเจกต์เรียนรู้และมารยาทการใช้ OSM Tile
Sheet รายการสกู๊ตเตอร์เมื่อกดพอร์ต สร้างด้วย DraggableScrollableSheetสร้าง UI เลือกตัวรถภายในพอร์ตด้วย BottomSheet — การจับคู่ DraggableScrollableSheet + FutureBuilder ของ Flutter

■ Backend / การตัดทอนแบบ dev profile

หัวข้อบทความ
ดึง PoC ขึ้นสู่ “เสมือนผ่าน authentication แล้ว” ด้วย filter น้อยที่สุดผ่อนคลายการยืนยันตัวตนเฉพาะใน dev ด้วย Spring Profile — Filter ขั้นต่ำที่ไม่กระทบโครงสร้าง prod
Inject ข้อมูล seed ตายตัว (พอร์ตรอบสถานี + กรณีผิดปกติ) เมื่อ startupรีเซต Dev DB ให้อยู่สถานะเดียวกันทุกครั้งด้วย DevPortSeedInitializer — CommandLineRunner ของ Spring Boot ในทางปฏิบัติ

■ Operations ที่ทำให้ design กับ implementation อยู่ในแนวเดียวกัน

หัวข้อบทความ
Divergence register, implementation-diff task, ตรวจสามจุด เพื่อปกป้อง “ต้นฉบับ”ไม่ให้ implementation ห่างจาก design — ตารางจัดการความเบี่ยงเบนและการตรวจสามจุดที่ปกป้อง ‘ต้นฉบับ’

อยู่ระหว่างดำเนินการ / ยังไม่ได้แตะ

อัปเดตตามความคืบหน้าของโปรเจกต์

  • Implementation ของ FIDO2/WebAuthn passkey จริง (ตอนนี้ bypass ใน dev profile)
  • เชื่อม Stripe สำหรับ payment จริง (ตอนนี้ mock ที่ service layer)
  • eKYC vendor integration (ตอนนี้ใช้ mock ที่ return คงที่)
  • Steady-state MQTT Broker + on-vehicle device pseudo-client
  • Fleet map บน admin console (Thymeleaf SSR)
  • Flow งานเปลี่ยนแบตเตอรี่บน swapper app

ปิดท้าย

เป้าหมายของโปรเจกต์นี้ไม่ใช่การ ship “บริการ e-scooter sharing ที่เสร็จสมบูรณ์” แต่เป็นการบันทึก “ถ้าฉันต้องสร้างบริการที่ทุกคนเห็นบนถนน ฉันต้องคิดอะไรบ้าง?” จาก design และ implementation

ดังนั้นแต่ละบทความไม่ใช่ “implementation ที่ถูกต้อง” แต่เป็นบันทึก “ในสถานการณ์นี้ฉันตัดสินใจแบบนี้ และนี่คือสิ่งที่ฉันยอมรับเป็น tradeoff” ถ้าสิ่งนี้ขยายพื้นที่ทางเลือกในครั้งที่คนอื่นเผชิญหัวข้อเดียวกัน นั่นคือชัยชนะ

อ่านตามลำดับใดก็ได้ ถ้าอยากตามลำดับการสร้าง แผนที่บทความข้างต้นโดยประมาณเป็นลำดับนั้น: E2E ขั้นต่ำ → mobile UI → การตัดทอน backend → operations ของ design

ส่งข้อความได้ตามสบาย

กรุณาส่งข้อความ หากมีคำปรึกษาด้านเทคนิค ความคิดเห็น หรือคำถาม