สร้าง E-Scooter Sharing ระดับท้องถนนตั้งแต่ศูนย์เพื่อทำความเข้าใจ — ชุดบันทึก Design, Implementation, และ Operations
บทนำ
ในช่วงไม่กี่ปีที่ผ่านมา พอร์ตสำหรับ 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 |
+----------------------+
| Application | Stack | กลุ่มผู้ใช้ |
|---|---|---|
| User App | Flutter (iOS/Android) | ผู้ขับขี่ |
| Swapper App | Flutter (iOS/Android) | คนเปลี่ยนแบตเตอรี่ / บำรุงรักษาภาคสนาม |
| Admin Console | Spring Boot + Thymeleaf (SSR) | มอนิเตอร์การดำเนินงานและควบคุมระยะไกล |
| Backend API | Java 21+ / Spring Boot 3.x | Platform กลางของ 3 แอป |
| Database | PostgreSQL + PostGIS | คำนวณ polygon ของพอร์ตจำเป็นต้องมี spatial extension |
| IoT | MQTT Broker → on-vehicle device | คำสั่งปลดล็อกและ telemetry |
| External service | FIDO2/SMS/eKYC/Stripe/maps/notifications | AuthN, KYC, payment, map |
External dependency (SMS / payment / eKYC / IoT) ถูก mock ภายใต้ dev profile ก่อน integration จริง เป้าหมายคือยืนยันว่า “ทุกหน้าจอ flow ครบ end-to-end” ก่อนเชื่อมของจริง บทความหลายชิ้นในชุดนี้พูดถึงการตัดสินใจตัดทอนแบบ dev profile นี้
การเลือกเทคโนโลยี
| Layer | Tech | เหตุผล |
|---|---|---|
| Mobile app | Flutter 3.x | Codebase เดียวสำหรับ iOS/Android |
| Backend API | Java 21 + Spring Boot 3.x | Track record ระดับ enterprise; JPA / Security / Profile พร้อมใช้ |
| Admin console | Spring Boot + Thymeleaf | ภาษาและ framework เดียวกับ backend ลด cognitive load |
| Database | PostgreSQL 17 + PostGIS | ตรวจ polygon พอร์ต, ค้นหา spatial ของสกู๊ตเตอร์ |
| IoT | MQTT | Low-latency bidirectional device messaging |
| Authentication | FIDO2/WebAuthn + JWT | Passkey เป็นหลัก; JWT สำหรับ stateless API session |
| State management (mobile) | Riverpod | Cross-screen state และ async fetch coordination |
| HTTP (mobile) | dio | เขียน interceptor ง่าย |
| Map rendering | flutter_map (OpenStreetMap) | ไม่ต้องกังวล license; เหมาะกับ development โดยไม่ต้อง setup บัญชี |
อภิธานศัพท์ — Screen ID และ Operational ID
ระบุที่มาจาก design doc ปรากฏตรง ๆ ในบทความ ใช้ตารางนี้เป็นการอ้างอิงเมื่อเจอ “S02” หรือคล้าย ๆ
Screen ID (User App, S01–S13)
| ID | หน้าจอ | บทบาท |
|---|---|---|
| S01 | Splash | App startup, ตรวจ session |
| S02 | Home map | แสดงพอร์ตและสกู๊ตเตอร์ว่างใกล้เคียงบนแผนที่ |
| S03 | QR scan | อ่าน QR ของสกู๊ตเตอร์ที่จะใช้ |
| S04 | เลือกพอร์ตปลายทาง | เลือกพอร์ตคืนรถจากแผนที่ |
| S05 | Login / 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-XXX | Divergence ID — หนึ่งหมายเลขต่อหนึ่งความเบี่ยงเบนระหว่าง design กับ implementation |
TASK-XX | Implementation-diff task ID — หนึ่งหมายเลขต่อหนึ่งงานที่ปิด divergence |
app-api | Scope 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