AI Web Studio977-121 · PSU PHUKET
บทเรียน
ดาวน์โหลด Markdown

AI Web Studio · 977-121 · PSU Phuket

บทเรียน 06 — เว็บเช่าอุปกรณ์: availability และ capacity ของ demo ที่รันได้จริง

ผลลัพธ์การเรียนรู้

ผู้เรียนจะใช้ /examples/rental/, GET /api/availability และ POST /api/rentals ของ project นี้, ตรวจ D1 table rental_days, และอธิบายได้ว่าทำไม demo ใช้ one row ต่อวันเพื่อกัน capacity แบบ atomic. Lab นี้จำลองการ reserve (status: "reserved-demo") เท่านั้น ไม่ใช่ contract การเช่าหรือ payment จริง.

ต้องมีก่อนเริ่ม

  • รัน npm run db:local และให้ npm run dev ทำงานที่ http://localhost:3320 ตามบท 05
  • ใช้ข้อมูลติดต่อสมมติเท่านั้น; DEMO_MODE=true redact name/email ก่อน insert แต่ยังมี validation, daily limit และ capacity constraint
  • เลือกวันเริ่มในอนาคตไม่เกิน 365 วัน, วันสิ้นสุดหลังวันเริ่ม 1–30 วัน. Demo ใช้ช่วง [start,end): รวมวันเริ่ม แต่ไม่รวมวันคืน

Prompt สำหรับสร้างเว็บให้เช่าอุปกรณ์

คัดลอก prompt นี้ไปใช้ในสำเนา starter ของผู้เรียนก่อนทำ lab ตัวอย่างใช้ชื่อ “หยิบยืม” และ contract เดียวกับหน้าเว็บที่เปิดดูได้ ภาพหน้าจอเป็นผลลัพธ์อ้างอิงสำหรับเทียบงาน ส่วนการกันจองเกินจำนวนต้องพิสูจน์จาก API และ D1

สร้างเว็บไซต์ให้เช่าอุปกรณ์ถ่ายภาพภาษาไทยชื่อ “หยิบยืม” ใน starter หลักสูตรนี้
ให้ผู้เรียนเลือกกล้อง ไฟ หรือชุดเสียง เลือกช่วงวันและจำนวน ตรวจของว่าง และจองสาธิตได้
อ่าน package.json, examples/rental/, src/worker.ts และ migrations/0001_initial.sql
กับ 0002_demo_retention.sql ก่อนลงมือ เพื่อใช้ contract และกติกาที่มีอยู่จริง
ใช้ HTML/CSS/vanilla JavaScript ไม่เพิ่ม dependency ไม่เพิ่มระบบชำระเงิน
สรุปแผนและ state ของฟอร์ม แล้วสร้าง examples/rental/{index.html,style.css,main.js}
รักษา API เดิมและเว็บอื่น ไม่แก้ migration ที่ apply แล้ว; ถ้าขาด API ให้รายงาน prerequisite

แนวภาพ: เหลืองสด ดำ ขาว จัดข้อมูลอุปกรณ์ให้ดูง่ายและใช้งานได้เร็ว
ใช้ฟอนต์ไทยใน public/fonts และภาพ /images/rental-kit.webp
มี hero, รายการอุปกรณ์, ตัวเลือกช่วงวัน/จำนวน, ผลตรวจของว่าง,
ฟอร์มข้อมูลสาธิต, ขั้นตอนการเช่า และข้อความว่าการจองไม่มีผลทางการค้า
โหลดอุปกรณ์ ราคา และ stock จาก GET /api/catalog ไม่ใช้ราคาที่ hard-code เป็น authority

ฟอร์มใช้ equipmentId, start, end, quantity โดยวันที่เป็นปฏิทิน UTC
ใช้ช่วง [start,end): รวมวันรับ ไม่รวมวันคืน ช่วงเช่า 1–30 วันตาม server validation
เรียก GET /api/availability?equipmentId=...&start=...&end=...&quantity=...
แสดง loading/available/unavailable/error และ totalPrice/currency/days ตาม API
เมื่อเปลี่ยนตัวเลือก ให้ปิดการส่งทันที ยกเลิกผลเก่า และป้องกัน response ที่มาช้าทับค่าปัจจุบัน

ส่ง JSON {equipmentId,start,end,quantity,name,email,consent,website} ไป POST /api/rentals
ส่ง Idempotency-Key และใช้ key เดิมเมื่อ retry ข้อมูลเดิม
ไม่ส่ง price,totalPrice,stock หรือ status ให้ server เชื่อจาก client
D1 และ capacity trigger เป็นผู้ตัดสินจำนวนว่างตอนเขียนข้อมูล; preview ไม่ใช่การยืนยันจอง
เมื่อ 201 แสดง id,totalPrice,status และ expiresAt; demo hold หมดอายุภายใน 30 นาที
เมื่อ 409 UNAVAILABLE ให้แจ้งว่าของเพิ่งถูกจองและตรวจ availability ใหม่
แสดง error.message ให้ผู้ใช้เข้าใจ เก็บข้อมูลฟอร์มไว้แก้ไขหรือ retry
ใช้ข้อมูลติดต่อสมมติ แจ้งการปกปิดชื่อ/อีเมลก่อนบันทึก D1 และไม่อ้างว่าเป็นสัญญาเช่าจริง

ทุก input มี label ใช้ keyboard ได้ มี focus/status ชัดเจน และมือถือ 390px ไม่ล้น
ตรวจ npm run build, npm run lint และใช้ชุดทดสอบ API เดิมสำหรับวันไม่ถูกต้อง,
ช่วงติดกันไม่ทับกัน, ของเต็ม, concurrent requests และ idempotency
ใช้เฉพาะพอร์ตที่จองไว้และ local D1 ของชุดทดสอบ ไม่ reset ฐานข้อมูลจริง
ส่งสรุป source, ภาพหน้าเว็บ, request/response ตัวอย่าง และผลทดสอบที่รันจริง

ผลลัพธ์ที่ควรได้: เว็บ หยิบยืม มีตัวเลือกอุปกรณ์ วัน และจำนวน พร้อมผล availability และเลขอ้างอิงเมื่อจองสาธิตสำเร็จ เทียบ ภาพผลลัพธ์ และ prompt ภาพอุปกรณ์ แล้วพิสูจน์เรื่อง stock ด้วย lab ด้านล่าง

Prompt ติดตามผลหลังสร้าง

ตรวจเว็บให้เช่าที่เพิ่งสร้าง แล้วแก้กรณีที่ไม่ผ่านโดยรักษา API contract เดิม
ลองเปลี่ยนวันอย่างรวดเร็วระหว่างโหลด, วันคืนเท่ากับวันรับรายการถัดไป,
ของไม่พอ, POST ตอบ 409, ส่งคำขอเดิมซ้ำ และเปิดบนมือถือ 390px
ตรวจว่าผลของ query เก่าไม่เปิดปุ่มจองให้ข้อมูลใหม่ และราคา/stock มาจาก D1
รายงานผลพร้อม request/response และหลักฐานจากชุดทดสอบ ไม่สรุปว่าจองได้จากข้อความบน UI อย่างเดียว

Contract และ schema ที่มีอยู่จริง

แบบจำลองข้อมูลและการเชื่อมต่อของระบบเช่า โดยหน้า examples rental เรียก availability และ rentals ซึ่งอ่านเขียน equipment rentals และ rental_days ก่อน capacity trigger ตรวจจำนวนต่ออุปกรณ์ต่อวันแบบจำลองข้อมูลและการเชื่อมต่อของระบบเช่า โดยหน้า examples rental เรียก availability และ rentals ซึ่งอ่านเขียน equipment rentals และ rental_days ก่อน capacity trigger ตรวจจำนวนต่ออุปกรณ์ต่อวัน
บนจอเล็ก เลื่อนแนวนอนเพื่ออ่านแผนภาพ หรือเปิดไฟล์ HTML จากลิงก์ใต้ภาพ

เปิดแผนภาพขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง

ความสัมพันธ์เดิมยังครบ: UI ส่ง GET equipmentId,start,end,quantity ไป /api/availability และส่ง POST ฟิลด์เดียวกันพร้อมข้อมูลติดต่อสาธิตไป /api/rentals; availability อ่าน equipment + rental_days; rentals เขียนชุดคำสั่งด้วย D1 batch และ capacity trigger ตรวจข้อมูลใน write path

ลำดับการตรวจของว่างและสร้างรายการจองสาธิต โดยผล availability เป็นเพียง preview ก่อน Worker ตรวจซ้ำและให้ D1 capacity trigger เป็นผู้ตัดสินขณะเขียนลำดับการตรวจของว่างและสร้างรายการจองสาธิต โดยผล availability เป็นเพียง preview ก่อน Worker ตรวจซ้ำและให้ D1 capacity trigger เป็นผู้ตัดสินขณะเขียน
บนจอเล็ก เลื่อนแนวนอนเพื่ออ่านแผนภาพ หรือเปิดไฟล์ HTML จากลิงก์ใต้ภาพ

เปิดลำดับคำขอขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง

ฝึกอ่านภาพ: อธิบายสถานการณ์ที่ GET ตอบว่าว่างแต่ POST ตอบ 409 UNAVAILABLE แล้วชี้ว่าจุดใดป้องกัน overbooking จริง

Worker รับ query equipmentId, start, end, quantity ที่ /api/availability, และ POST body เดียวกันเพิ่ม name, email, consent, website ที่ /api/rentals. เมื่อ DB binding พร้อม Worker อ่าน equipment, daily rate และ stock จาก D1; migration 0001_initial.sql seed ตาราง equipment และมี rentals, rental_days กับ trigger rental_days_capacity_before_insert.

rental_days แตกแต่ละ reservation เป็นหนึ่ง row ต่อวัน. Trigger รวม quantity ของ equipment/date เดียวกัน แล้ว abort ด้วย capacity_exceeded หากมากกว่า equipment.stock. Worker ส่ง insert reservation, insert days และ idempotency record ผ่าน db.batch(), ซึ่ง D1 ระบุว่าเป็น transaction: statement ทำตามลำดับและ rollback sequence เมื่อ statement ใดล้มเหลว (D1 batch()). นี่ทำให้ availability preview ไม่ใช่ authority; capacity ถูกตรวจอีกครั้งตรง write path.

Lab: ตรวจ availability และสร้าง reservation สาธิต

  1. ถ้ายังไม่ได้เปิด local environment ให้ใช้ command จริงของ project:

    npm run db:local
    npm run dev
    

    ผลที่คาดหวัง: local D1 มี tables จาก migrations/0001_initial.sql และหน้า http://localhost:3320/examples/rental/ โหลด catalog จาก /api/catalog.

  2. กำหนดช่วงวันแบบ portable ด้วย Node (ไม่ hard-code วันที่ที่จะหมดอายุในหลักสูตร):

    START=$(node -e "console.log(new Date(Date.now()+7*864e5).toISOString().slice(0,10)")
    END=$(node -e "console.log(new Date(Date.now()+8*864e5).toISOString().slice(0,10)")
    echo "$START to $END"
    
  3. เรียก availability endpoint ที่มีจริง:

    curl -sS "http://localhost:3320/api/availability?equipmentId=camera&start=$START&end=$END&quantity=1"
    

    ผลที่คาดหวัง: JSON มี available, remaining, totalPrice, currency, days; เช่น {"available":true,"remaining":2,"totalPrice":189000,"currency":"THB","days":1} เมื่อยังไม่มี reservation. totalPrice เป็น integer satang และ currency เป็น THB.

  4. POST reservation demo. Origin ต้องตรง local Worker, consent ต้อง true, honeypot website ต้องว่าง และ key นี้ใช้ซ้ำได้เฉพาะ payload เดิม:

    curl -i -X POST http://localhost:3320/api/rentals \
      -H 'Origin: http://localhost:3320' \
      -H 'Content-Type: application/json' \
      -H 'Idempotency-Key: rental-lab-001' \
      --data "{\"equipmentId\":\"camera\",\"start\":\"$START\",\"end\":\"$END\",\"quantity\":1,\"name\":\"Demo Learner\",\"email\":\"learner@example.test\",\"consent\":true,\"website\":\"\"}"
    

    ผลที่คาดหวัง: HTTP 201 และ {id,totalPrice,currency:"THB",status:"reserved-demo",message}. ข้อความจริงคือ จองอุปกรณ์ในระบบสาธิตแล้ว รายการนี้ไม่มีผลทางการค้า; จึงห้ามเปลี่ยน copy ของ lesson/page เป็น “ชำระเงินสำเร็จ”.

  5. Inspect rows ที่ Worker สร้างจริง:

    npx wrangler d1 execute DB --local --command \
      "SELECT rental_id, equipment_id, rental_date, quantity FROM rental_days ORDER BY rental_date"
    npx wrangler d1 execute DB --local --command \
      "SELECT id, status, name, email, total_price FROM rentals ORDER BY created_at DESC LIMIT 3"
    

    ผลที่คาดหวัง: วันใน [START, END) มี row ใน rental_days; rentals.status เป็น reserved-demo, และ name/email เป็น [demo-redacted].

  6. ทดสอบ validation และ conflict. ลอง end=$START จะได้ 400 INVALID_DURATION; ลอง quantity มากกว่า stock ของ camera (3) จะได้ 400 INVALID_FIELD. สร้าง/ยิง requests ซ้อนกันจนเกิน stock ในวันเดียวกัน แล้ว Worker จะ map SQLite trigger error เป็น 409 envelope นี้:

    {"error":{"code":"UNAVAILABLE","message":"อุปกรณ์ไม่พอสำหรับช่วงวันที่เลือก"}}
    

    ทดสอบ boundary โดยสร้าง A [START,END) และ B เริ่ม END: สองรายการไม่แชร์ rental_date จึงไม่ชนกัน. B ที่เริ่มก่อน END หนึ่งวันใช้วันร่วมกันและกิน stock วันนั้น. นี่เป็นผลของ half-open interval ที่ schema/recursive SQL ทำจริง.

  7. ตรวจ browser path ต่อ: เปิด /examples/rental/, เลือก camera และวันเดียวกับ command, ดู response จาก /api/availability, ส่ง form ด้วย demo identity. จากนั้นรัน:

    npm run typecheck
    npm run test
    npm run build
    

ขอบเขต demo กับ business implementation

เครื่องสถานะของแถวการจองสาธิตจาก reserved-demo ไปสู่การหมดผลตาม expires_at หลัง 30 นาที แล้วถูก scheduled cleanup รายชั่วโมงลบออกเครื่องสถานะของแถวการจองสาธิตจาก reserved-demo ไปสู่การหมดผลตาม expires_at หลัง 30 นาที แล้วถูก scheduled cleanup รายชั่วโมงลบออก
บนจอเล็ก เลื่อนแนวนอนเพื่ออ่านแผนภาพ หรือเปิดไฟล์ HTML จากลิงก์ใต้ภาพ

เปิดแผนภาพสถานะขนาดเต็ม พร้อม prompt และแหล่งข้อมูลที่ใช้สร้าง

ภาพนี้แสดงเฉพาะสิ่งที่โค้ดทำแล้ว: รุ่นปัจจุบันคืน reserved-demo; “หมดผล” เป็นเงื่อนไขเวลาและ “deleted” หมายถึงไม่มีแถวแล้ว ไม่ใช่ค่าในคอลัมน์ status ส่วนสถานะ quote, hold, confirmed และ cancelled ต้องมี schema, authorization และ test เพิ่มก่อนอ้างว่าใช้งานจริง

เรื่อง DEMO_MODE=true ปัจจุบัน งานผลิตจริงภายหลัง
state ทุก POST สำเร็จเป็น reserved-demo และกิน local capacity นิยาม quote/hold/confirmed/cancelled, hold expiry, staff authorization และ audit
อุปกรณ์/ราคา D1 seeded catalog เป็น authority ของ availability/price; D1 เก็บ reservation product/catalog administration ที่มี authorization และ migration strategy
data redact PII, ไม่มี notification/payment data policy, staff access, communication และ lawful retention
capacity trigger per equipment-day ใน D1 load/concurrency, cancellation/release rule และ reconciliation ตามธุรกิจ

ตารางขวาเป็น conceptual alternative ไม่ใช่สิ่งที่ code ใน demo อ้างว่าทำแล้ว. หากออกแบบ state จริง อาจใช้ quote_request ที่ไม่กิน capacity, hold ที่มี expiry และ confirmed_booking ที่กิน capacity; ต้องเพิ่ม schema/authorization/test ใหม่ทั้งหมด. Current demo ตั้งใจสอน transaction/capacity โดยไม่รับเงินจริง.

OWASP แนะนำ validate ทั้งรูปแบบและ business rule ตั้งแต่รับ input (Input Validation); Worker ตรวจ date จริง, duration, date in past/too far, equipment ID และ quantity ก่อนสร้าง statements. D1 statement ใช้ bind parameters ไม่ต่อ user input เป็น SQL (D1 Worker Binding API, OWASP SQL injection).

ข้อผิดพลาดที่พบบ่อย

อาการ สาเหตุ วิธีแก้
404 หรือ response ไม่ใช่ API พิมพ์ endpoint ไม่ตรง Worker ใช้ /api/availability และ /api/rentals เท่านั้น
403 ORIGIN_REJECTED POST ไม่มี Origin ที่ตรง Worker ใช้ header ตาม lab
400 DATE_IN_PAST คัดลอกวันที่เก่าจากตัวอย่าง สร้าง START/END จากขั้น 2 ใหม่
409 UNAVAILABLE reservation อื่นกิน quantity ในวันนั้น เป็น expected business response; เลือกวัน/จำนวนใหม่แล้วเช็ก availability
ไม่เห็นข้อมูลติดต่อใน D1 demo redaction ทำงาน ตรวจ [demo-redacted]; อย่าใช้ PII เพื่อ “แก้” ผลนี้

แบบประเมิน

ผู้เรียนสาธิต GET availability, POST 201, D1 rental_days และ 409 capacity conflict. ต้องชี้ได้ว่าทำไม GET เป็น preview แต่ trigger/batch คือจุดตัดสินจริง และบอกได้ว่า reserved-demo ต่างจากสัญญาเช่าหรือ payment confirmation อย่างไร.

อ่านต่อ: มาตรฐานคุณภาพและ commerce และ บท deploy.

บทถัดไป →

ความคืบหน้าบันทึกเฉพาะเบราว์เซอร์นี้ ไม่มีบัญชีผู้เรียนหรือการส่งข้อมูลการเรียนขึ้นเซิร์ฟเวอร์