แอปฟิตเนสอัจฉริยะ: โครงสร้างข้อมูล สคีมาฐานข้อมูล และข้อกำหนดการออกแบบเซิร์ฟเวอร์
Artifactนี่คืออาร์ติแฟกต์ที่ใช้ในตอนที่ 2: วางแผนข้อกำหนดของระบบแอป (ไม่มีโค้ด) คุณสามารถใช้ข้อกำหนดนี้เป็นข้อมูลอ้างอิงสำหรับการจัดโครงสร้างคอลเล็กชัน Cloud Firestore, ปลายทางเซิร์ฟเวอร์ และหน่วยความจำของ Agent แบบ 3 ระดับ
ลงชื่อเข้าใช้เพื่อเข้าถึงอาร์ติแฟกต์นี้
ลงชื่อเข้าใช้ด้วยโปรไฟล์นักพัฒนาแอปของ Google เพื่อดูและคัดลอกอาร์ติแฟกต์ทั้งหมดที่ใช้ในซีรีส์ Build It!
ต้องมีโปรไฟล์นักพัฒนาแอป
คุณไม่มีสิทธิ์สร้างโปรไฟล์นักพัฒนาแอป แจ้งให้ผู้ดูแลระบบ Workspace เปิดใช้ฟีเจอร์นี้
สร้างโปรไฟล์นักพัฒนาซอฟต์แวร์
สร้างโปรไฟล์นักพัฒนาแอปของ Google ฟรีเพื่อปลดล็อกพรอมต์และอาร์ติแฟกต์ทั้งหมดของ Build It!
เข้าร่วม Builder Central เพื่อปลดล็อก
รับป้ายการเป็นสมาชิก Builder ฟรีในคลิกเดียวเพื่อปลดล็อกพรอมต์และอาร์ติแฟกต์ทั้งหมดของ Build It!
สถานะ: กำลังดำเนินการ (ร่างด้วย Planning Companion)
ฐานข้อมูลเป้าหมาย: Google Cloud Firestore
เป้าหมายสถาปัตยกรรม: การสร้างต้นแบบฟิตเนสอัจฉริยะของ AI Studio ขึ้นมาใหม่ให้สะอาดตา บำรุงรักษาได้ และพร้อมใช้งานจริง
0. สแต็กทางเทคนิคหลักและการตัดสินใจเกี่ยวกับสถาปัตยกรรมระบบ
ระบบจะล็อกเกณฑ์พื้นฐานทางสถาปัตยกรรมต่อไปนี้สำหรับการติดตั้งใช้งานทั้งหมด เพื่อให้มั่นใจว่าการใช้งานจริงมีความเสถียร มีประสิทธิภาพ และมีขอบเขตที่ชัดเจน
- เฟรมเวิร์กฟรอนท์เอนด์: Next.js (App Router, React 19) พร้อมการแคชเนื้อหาออฟไลน์ของ Progressive Web App (PWA) (โดยใช้ Serwist /
@ducanh2912/next-pwa) - การจัดรูปแบบและคอมโพเนนต์ UI: Tailwind CSS v4 + Tailwind Variants (สำหรับการจัดการตัวแปร) + Lucide React (ไอคอน)
- สถาปัตยกรรมสถานะไคลเอ็นต์
- Zustand (พร้อมมิดเดิลแวร์
persist): จัดการสถานะไคลเอ็นต์ในเครื่อง สถานะไคลเอ็นต์ที่สัมผัสได้ และสถานะไคลเอ็นต์ชั่วคราว (ตัวจับเวลาของนาฬิกาจับเวลา บัฟเฟอร์เซ็ตที่ใช้งานอยู่ กราฟจำนวนก้าว และข้อมูลที่ป้อนในยิมที่ยังไม่ได้บันทึก) - TanStack Query (React Query): จัดการสถานะเซิร์ฟเวอร์แบบอะซิงโครนัสทั้งหมด, การแคชเอกสาร Firestore, การตรวจสอบความถูกต้องอีกครั้งในเบื้องหลัง, การกรองคำขอที่ซ้ำกันออก และการเปลี่ยนแปลงในเชิงบวก
- Zustand (พร้อมมิดเดิลแวร์
- รันไทม์แบ็กเอนด์และการโฮสติ้ง API: เครื่องจัดการเส้นทาง Next.js (
app/api/...) ที่โฮสต์ใน Google Cloud Run ในภูมิภาคเดียวกับ Firestore และแคชบริบทของ Gemini เพื่อให้มีเวลาในการตอบสนองภายในต่ำกว่า 15 มิลลิวินาที - การตรวจสอบสิทธิ์: การตรวจสอบสิทธิ์ Firebase (การลงชื่อเข้าใช้ Google, อีเมล/รหัสผ่าน และการลิงก์การเริ่มต้นใช้งานแบบไม่ระบุตัวตน) ไคลเอ็นต์จะแนบ
Authorization: Bearer <Firebase_ID_Token>ในคำขอ API ทั้งหมด และเซิร์ฟเวอร์จะยืนยันตัวตนโดยใช้firebase-admin.auth().verifyIdToken(token) - ฐานข้อมูลหลัก: Google Cloud Firestore
1. ภาพรวมของระบบและเส้นทางของผู้ใช้หลัก
1.1 ขอบเขตและหลักการด้านสถาปัตยกรรม
ข้อกำหนดนี้จะกำหนดโมเดลข้อมูล, สคีมา Cloud Firestore, สัญญาปลายทางของเซิร์ฟเวอร์ และโครงสร้างหน่วยความจำของ AI Agent สำหรับแอปพลิเคชัน Smart Fitness โดยจะสร้างแหล่งข้อมูลที่เชื่อถือได้เพียงแหล่งเดียว (SSOT) สำหรับการวางแผนการออกกำลังกายทั้งหมด การบันทึกเซสชันที่ผ่านมา และหน่วยความจำการฝึกสอนแบบแบ่งระดับอัตโนมัติ ซึ่งแยกออกจากเฟรมเวิร์กการนำเสนอส่วนหน้าโดยสิ้นเชิง (ซึ่งจะกล่าวถึงในข้อกำหนดการออกแบบ UI/UX โดยเฉพาะ)
1.2 หลักการสำคัญเกี่ยวกับข้อมูล
- การแยกความรับผิดชอบ: ขอบเขตที่เข้มงวดระหว่างสัญญาข้อมูล (แบ็กเอนด์/ฐานข้อมูล) และการออกแบบ UI ของไคลเอ็นต์
- สถาปัตยกรรมคลังแบบผสม: สารานุกรมการออกกำลังกายแบบคงที่และเปลี่ยนแปลงไม่ได้ (
exercises) ควบคู่ไปกับการจัดตารางเวลารายสัปดาห์แบบไดนามิกที่ใช้ AI (weekly_plans) - การเปลี่ยนแปลงแบบมีมนุษย์ร่วมที่จัดเตรียมไว้: การเปลี่ยนแปลงการออกกำลังกายที่ AI สร้างขึ้นจะแสดงเป็นข้อเสนอที่จัดเตรียมไว้ การเขียนฐานข้อมูลจะเกิดขึ้นเมื่อผู้ใช้ยอมรับอย่างชัดแจ้งเท่านั้น
- ความทรงจำแบบต่อเนื่อง 3 ระดับ: AI จะบันทึก อัปเดต และล้างข้อมูลข้อเท็จจริงของผู้ใช้โดยอัตโนมัติในข้อมูลถาวร สถานะทางกายภาพที่หมดอายุ (พร้อม TTL ที่ชัดเจน) และบริบทตามสถานการณ์ชั่วคราว
- การอ่านเอกสารเดียวแบบละเอียด: ข้อมูลจะได้รับการจัดโครงสร้างเป็นเอกสารที่เพิ่มประสิทธิภาพ (เช่น สมุดบันทึกแบบเกลียวสำหรับแผนรายสัปดาห์และที่เก็บข้อมูลแชทรายสัปดาห์) เพื่อลดเวลาในการตอบสนองของการอ่านและกำจัดการค้นหาที่ซ้ำซ้อน
2. ปลายทางของเซิร์ฟเวอร์และสัญญา API
ปลายทางทุกรายการต้องระบุแหล่งที่มาของการตอบกลับ (
AGENT,DATABASEหรือHYBRID) เพื่อให้สามารถกำหนดค่าปลายทางที่ทำงานด้วย Agent ได้อย่างเหมาะสมในระหว่างการติดตั้งใช้งาน
POST /api/coach/chat
- วัตถุประสงค์: การโต้ตอบแบบสนทนากับเทรนเนอร์ส่วนบุคคล AI, การจัดการคำถามเกี่ยวกับการฝึกสอน, การสนับสนุนด้านแรงจูงใจ และการสร้างการปรับเปลี่ยนการออกกำลังกายที่เสนอ (ทั้งการปรับเปลี่ยนรายวันและการยกเครื่องรายสัปดาห์)
- ทริกเกอร์: ผู้ใช้ส่งข้อความในแท็บโค้ชหรือแตะชิปแก้ไขที่แนะนำตามบริบทในบรรทัด (เช่น "แทนที่ Arnold Press ด้วย DB Shoulder Press")
- แหล่งที่มาของคำตอบ:
AGENT— ใช้ประโยชน์จาก Gemini ด้วยความสามารถในการเรียกใช้เครื่องมือ/ฟังก์ชันที่มีโครงสร้างและการสตรีม ไม่เปลี่ยนแปลงฐานข้อมูลโดยตรง แต่จะแสดงข้อเสนอที่จัดเตรียมไว้สำหรับการตรวจสอบแบบมีมนุษย์ร่วม - ส่วนหัว:
Authorization: Bearer <Firebase_ID_Token>,Content-Type: application/json
เพย์โหลดคำขอ:
{
"message": "string (user input or selected prompt chip text)",
"context": {
"active_tab": "string (enum: today | week | coach)",
"week_start_date": "string (ISO date YYYY-MM-DD)",
"selected_day": "string (e.g. monday, optional)"
}
}เพย์โหลดการตอบกลับ:
{
"message": "string (conversational coaching response displayed in chat bubble)",
"proposal": {
"id": "string (unique proposal ID, nullable)",
"type": "string (enum: NONE | UPDATE_DAY | UPDATE_WEEK)",
"target_day": "string (e.g. monday, nullable)",
"summary_diff": "string (concise summary of changes, e.g. 'Swapped Arnold Press ➔ DB Shoulder Press', nullable)",
"data": "map (full proposed workout day object or weekly plan object, nullable)"
},
"memory_mutations": [
{
"operation": "string (enum: CREATE | UPDATE | DELETE)",
"target_tier": "string (enum: tier_1_staples | tier_2_physiological | tier_3_situational)",
"memory_id": "string (required for UPDATE and DELETE)",
"entry": "map (memory payload with fact, category, expires_at, etc., required for CREATE and UPDATE)",
"reason": "string (concise rationale for why memory was mutated)"
}
]
}- การเขียนแบบ Dual-Write ไปยัง
weekly_chats: ในแต่ละเทิร์น เซิร์ฟเวอร์จะเก็บข้อความขาเข้าของผู้ใช้ (text) และการตอบกลับของโค้ช (text) ไว้ใน/users/{userId}/weekly_chats/{week_start_date}โดยใช้FieldValue.arrayUnion() - รูปแบบการเปลี่ยนแปลงแบบแบ่งระยะ: แสดงเพย์โหลดข้อเสนอโดยไม่แก้ไข
/weekly_plansจนกว่าผู้ใช้จะแตะ[ยอมรับแพ็กเกจ] - การจัดการหน่วยความจำอัตโนมัติอย่างต่อเนื่อง: ส่ง
memory_mutationsเพื่อสร้าง อัปเดต หรือล้างรายการจากโปรไฟล์ 3 ระดับของผู้ใช้
POST /api/workouts/log
- วัตถุประสงค์: บันทึกเซสชันการออกกำลังกายที่เสร็จสมบูรณ์ลงในที่เก็บถาวรการฝึกถาวรของผู้ใช้ คำนวณปริมาณ/จำนวนครั้งรวมขั้นสุดท้าย ตรวจสอบสถิติส่วนตัว (PR) และแจ้งว่าวันที่กำหนดไว้เสร็จสมบูรณ์แล้ว
- ทริกเกอร์: ผู้ใช้แตะ "ออกกำลังกายเสร็จแล้ว" บนหน้าจอการติดตามการออกกำลังกาย
- แหล่งที่มาของการตอบกลับ:
DATABASE- ธุรกรรมฐานข้อมูลโดยตรง/เขียนไปยัง Cloud Firestore
เพย์โหลดคำขอ:
{
"plan_reference": {
"week_start_date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. monday)"
},
"title": "string (e.g. Upper Body Hypertrophy)",
"started_at": "string (ISO timestamp)",
"completed_at": "string (ISO timestamp)",
"duration_seconds": "number (e.g. 3120)",
"exercises_logged": [
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"exercise_name": "string (e.g. Incline DB Bench Press)",
"sets": [
{
"set_number": "number (e.g. 1)",
"weight_lbs": "number (e.g. 60)",
"reps_completed": "number (e.g. 12)",
"completed": "boolean"
}
]
}
],
"user_notes": "string (optional subjective feedback)",
"perceived_exertion": "number (optional 1-10 RPE rating)"
}เพย์โหลดการตอบกลับ:
{
"success": "boolean",
"log_id": "string (newly created workout log document ID)",
"summary": {
"total_volume_lbs": "number",
"total_sets_completed": "number",
"total_reps_completed": "number"
},
"new_prs": [
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"metric": "string (e.g. '10 Rep Max' or 'Estimated 1RM')",
"value": "string (e.g. '65 lbs' or '105 lbs')"
}
]
}3. สถาปัตยกรรมฐานข้อมูล Cloud Firestore
คอลเล็กชัน: exercises
ลำดับชั้น: คอลเล็กชันรูท (/exercises/{exercise_id}) • วัตถุประสงค์: สารานุกรมการออกกำลังกายแบบคงที่ซึ่งมีมัลติมีเดีย สัญญาณบอกท่าทาง ข้อกำหนดด้านอุปกรณ์ และวิธีการ
{
"id": "string (canonical slug matching document ID, e.g. incline-db-bench-press)",
"name": "string (e.g. Incline DB Bench Press)",
"category": "string (e.g. Strength, Hypertrophy, Cardio, Mobility)",
"target_muscles": {
"primary": ["string (e.g. Upper Chest)"],
"secondary": ["string (e.g. Front Deltoids, Triceps)"]
},
"media": {
"video_url": "string (URL to demonstration video)",
"poster_url": "string (URL to video poster / thumbnail image)"
},
"instructions": ["string (step-by-step execution guidance)"],
"form_tips": ["string (critical cues and common pitfalls)"],
"equipment_required": ["string (e.g. Dumbbells, Incline Bench)"]
}คอลเล็กชัน: weekly_plans
ลำดับชั้น: /users/{userId}/weekly_plans/{week_start_date} • วัตถุประสงค์: "สมุดบันทึกแบบเกลียว" ที่มีตารางการฝึกซ้อมรายสัปดาห์ที่สมบูรณ์ เมตริกสรุป และการออกกำลังกายในแต่ละวัน
{
"week_start_date": "string (ISO date YYYY-MM-DD, e.g. 2026-09-07)",
"theme": "string (e.g. Hypertrophy Focus & Upper Body Overload)",
"summary": {
"total_time_minutes": "number (raw minutes, e.g. 180)",
"total_volume_lbs": "number (raw tonnage in lbs, e.g. 38400)",
"frequency": "number (active workout days, e.g. 4)"
},
"suggested_edits": ["string (pre-generated AI prompt suggestion)"],
"days": {
"monday": {
"date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. Monday)",
"is_rest_day": "boolean",
"status": "string (enum: scheduled | completed | skipped)",
"completed_log_id": "string (nullable)",
"completed_at": "string (ISO timestamp, nullable)",
"title": "string (e.g. Upper Body Hypertrophy)",
"focus_tag": "string (e.g. Push & Chest Dominant)",
"duration_minutes": "number",
"estimated_volume_lbs": "number",
"coach_note": "string",
"muscle_focus": [{ "muscle": "string", "intensity": "number (0-100)" }],
"suggested_edits": ["string"],
"blocks": [
{
"label": "string (e.g. Main Compound Block)",
"exercises": [
{
"exercise_id": "string",
"name": "string",
"target_sets": "number",
"target_reps": "string or number",
"rest_seconds": "number"
}
]
}
]
}
}
}คอลเล็กชัน: weekly_chats
ลำดับชั้น: /users/{userId}/weekly_chats/{week_start_date} • วัตถุประสงค์: "Weekly Chat Bucket" ที่มีประวัติการสนทนาทั้งหมดสำหรับสัปดาห์การฝึกอบรมที่ระบุในการอ่านเอกสารเดียว
{
"week_start_date": "string (ISO date YYYY-MM-DD, e.g. 2026-09-07)",
"messages": [
{
"id": "string (unique message ID)",
"sender": "string (enum: user | coach)",
"text": "string (conversational message body)",
"created_at": "timestamp (server timestamp)",
"proposal": {
"id": "string (proposal UID, nullable)",
"type": "string (enum: UPDATE_DAY | UPDATE_WEEK, nullable)",
"target_day": "string (e.g. monday, nullable)",
"summary_diff": "string (nullable)",
"status": "string (enum: pending | accepted | dismissed)",
"data": "map (workout or week snapshot payload, nullable)"
}
}
]
}คอลเล็กชัน: workout_logs
ลำดับชั้น: /users/{userId}/workout_logs/{log_id} • วัตถุประสงค์: ที่เก็บถาวรแบบถาวรและเปลี่ยนแปลงไม่ได้ของเซสชันการออกกำลังกายที่เสร็จสมบูรณ์แล้ว
{
"id": "string (document ID)",
"plan_reference": {
"week_start_date": "string (ISO date YYYY-MM-DD)",
"day_of_week": "string (e.g. monday)"
},
"title": "string (e.g. Upper Body Hypertrophy)",
"started_at": "timestamp",
"completed_at": "timestamp",
"duration_seconds": "number",
"summary": {
"total_volume_lbs": "number",
"total_sets_completed": "number",
"total_reps_completed": "number"
},
"exercises_logged": [
{
"exercise_id": "string",
"exercise_name": "string",
"sets": [
{
"set_number": "number",
"weight_lbs": "number",
"reps_completed": "number",
"completed": "boolean"
}
]
}
],
"user_notes": "string (nullable)",
"perceived_exertion": "number (1-10 RPE, nullable)"
}คอลเล็กชัน: user_profiles
ลำดับชั้น: /users/{userId}/profile/main • วัตถุประสงค์: "แผนภูมิไคลเอ็นต์แบบเรียลไทม์" ที่จัดเก็บโปรไฟล์หน่วยความจำ 3 ระดับของผู้ใช้
{
"user_id": "string (document owner UID)",
"display_name": "string (e.g. Alex)",
"email": "string (e.g. alex@example.com)",
"preferences": {
"units": "string (enum: lbs | kg)",
"haptics": "boolean",
"sound": "boolean"
},
"equipment_ids": ["string (e.g. 'dumbbells', 'bench_adjustable', 'pull_up_bar')"],
"onboarding_status": "string (enum: not_started | in_progress | completed)",
"created_at": "timestamp",
"tier_1_staples": [
{
"id": "string (e.g. mem_01)",
"category": "string (enum: equipment | preference | goal | chronic_condition)",
"fact": "string (e.g. Home gym with dumbbells up to 50 lbs and adjustable bench)",
"source": "string (e.g. conversational_onboarding | manual_entry)"
}
],
"tier_2_physiological": [
{
"id": "string (e.g. mem_02)",
"body_part": "string (e.g. hamstrings, shoulder)",
"condition": "string (e.g. soreness, acute_fatigue, minor_strain)",
"fact": "string (e.g. Hamstrings are moderately sore from hill sprints)",
"effective_date": "string (ISO date YYYY-MM-DD)",
"expires_at": "timestamp"
}
],
"tier_3_situational": [
{
"id": "string (e.g. mem_03)",
"event": "string (e.g. golf_competition, travel)",
"fact": "string (e.g. Major golf tournament on Saturday; avoid excessive arm/lat fatigue on Friday)",
"week_start_date": "string (ISO date YYYY-MM-DD)",
"expires_at": "timestamp"
}
]
}คอลเล็กชัน: prs
ลำดับชั้น: /users/{userId}/prs/{exercise_slug} • วัตถุประสงค์: จัดเก็บสถิติส่วนตัวตลอดเวลา, เกณฑ์มาตรฐาน 1-Rep Max โดยประมาณ และสถิติเฉพาะจำนวนครั้ง
{
"exercise_id": "string (canonical slug, e.g. incline-db-bench-press)",
"exercise_name": "string (e.g. Incline DB Bench Press)",
"estimated_1rm": "number (calculated using Brzycki formula, e.g. 105)",
"max_weight_set": {
"weight_lbs": "number (e.g. 80)",
"reps": "number (e.g. 10)",
"achieved_at": "timestamp"
},
"rep_records": {
"10": 80,
"8": 85,
"5": 95
},
"last_updated": "timestamp"
}4. วงจรข้อมูลและไปป์ไลน์ของ Agent
4.1 ไปป์ไลน์ 1: การฝึกสอน การสร้างการออกกำลังกาย และการปรับเปลี่ยนตามระยะ
- การดำเนินการของผู้ใช้: ส่งข้อมูลที่ป้อนแบบอิสระหรือแตะชิปการแก้ไขตามบริบทที่สร้างไว้ล่วงหน้า
- การประกอบบริบท: เซิร์ฟเวอร์จะดึงข้อมูลโปรไฟล์หน่วยความจำ 3 ระดับที่ใช้งานอยู่และแผนของสัปดาห์ปัจจุบัน โดยตัดรายการหน่วยความจำที่หมดอายุออก
- การอนุมานของ Agent: Gemini จะประมวลผลพรอมต์และบริบทโดยใช้การเรียกใช้ฟังก์ชันที่มีโครงสร้าง
- การเปลี่ยนแปลงความทรงจำโดยอัตโนมัติ: ระบบจะนำข้อเท็จจริงที่ดึงมาจากผู้ใช้ไปใช้กับ
/user_profiles/{userId}และซิงค์กับequipment_ids - Staged Proposal Staging: การแก้ไขการออกกำลังกายที่เสนอจะแสดงผลพร้อม
proposal.status = "pending"ภายใน/weekly_chats/{week_start_date} - การแก้ไขและการคงอยู่ของผู้ใช้: การแตะ[ยอมรับแผน] จะยืนยันข้อมูลที่เสนอไปยัง
/weekly_plansและทำเครื่องหมายข้อเสนอเป็น"accepted"
4.2 ไปป์ไลน์ 2: การดำเนินการและการเก็บถาวรการออกกำลังกาย (ข้อมูลที่เป็นความจริง)
- การบันทึกเซสชันแบบเรียลไทม์: ไคลเอ็นต์จะติดตามน้ำหนักที่ยกจริง จำนวนครั้งที่ทำเสร็จ และแฟล็กการทำเสร็จในสถานะ UI ในเครื่อง
- การส่งเซสชัน: ไคลเอ็นต์เรียกใช้
POST /api/workouts/logเมื่อ "ออกกำลังกายเสร็จ" - การคำนวณและการตรวจสอบแบบรวม: เซิร์ฟเวอร์จะคำนวณปริมาณเซสชันสะสมและตรวจสอบ PR โดยใช้สูตร Brzycki
- Immutable Persistence: เขียนระเบียนที่เปลี่ยนแปลงไม่ได้ใน
/workout_logsทำเครื่องหมายวันที่"completed"ใน/weekly_plansและอัปเดตหรือแทรก PR ใหม่ลงใน/prs
4.3 ไปป์ไลน์ 3: การนำเข้าและการทำให้หน่วยความจำอัตโนมัติเป็นโมฆะ
- การนำเข้า: ระบบจะดึงข้อเท็จจริงอย่างต่อเนื่องจากการโต้ตอบในการสนทนาทั้งหมด
- การจัดประเภท: ระดับ 1 (ข้อมูลหลัก ไม่มีวันหมดอายุ), ระดับ 2 (สถานะทางกายภาพ, TTL 3-4 วัน), ระดับ 3 (บริบทตามสถานการณ์, TTL ตามเหตุการณ์)
- การตัดแต่งและการทำให้ใช้งานไม่ได้: ระบบจะกรองรายการที่หมดอายุ (
current_timestamp > expires_at) ออกจากบริบทพรอมต์และล้างข้อมูลแบบ Lazy
5. กฎความปลอดภัยของ Firestore และข้อกำหนดดัชนีแบบผสม
5.1 กฎความปลอดภัย (firestore.rules)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
match /profile/{document=**} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /weekly_plans/{planId} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /weekly_chats/{chatId} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /prs/{exerciseSlug} { allow read, write: if request.auth != null && request.auth.uid == userId; }
match /workout_logs/{logId} {
allow read, create: if request.auth != null && request.auth.uid == userId;
allow update, delete: if false;
}
}
match /exercises/{exerciseId} {
allow read: if request.auth != null;
allow write: if false;
}
}
}5.2 ดัชนีการค้นหาแบบผสม (firestore.indexes.json)
{
"indexes": [
{
"collectionGroup": "workout_logs",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "completed_at", "order": "DESCENDING" }]
},
{
"collectionGroup": "weekly_plans",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "week_start_date", "order": "DESCENDING" }]
},
{
"collectionGroup": "prs",
"queryScope": "COLLECTION",
"fields": [{ "fieldPath": "last_updated", "order": "DESCENDING" }]
}
]
}6. บันทึกการเปลี่ยนแปลงและการตัดสินใจที่ล็อกไว้
| วันที่ | องค์ประกอบ/การตัดสินใจ | ประเภท | ล็อกโดย |
|---|---|---|---|
| การตั้งค่าเบื้องต้น | เริ่มต้นสแคฟโฟลด์ข้อกำหนดที่ว่างเปล่าแล้ว | การตั้งค่าระบบ | ทีม |
| 2026-09-04 | เพิ่มคอลเล็กชัน Firestore: exercises และ weekly_plans | สคีมา | ทีม |
| 2026-09-04 | เพิ่มปลายทาง POST /api/coach/chat และคอลเล็กชัน chat_messages | ปลายทางและสคีมา | ทีม |
| 2026-09-04 | เปลี่ยนโครงสร้างภายในโค้ดของพื้นที่เก็บข้อมูลแชทเป็นสคีมาที่เก็บข้อมูล weekly_chats | สคีมา | ทีม |
| 2026-09-04 | เพิ่มปลายทาง POST /api/workouts/log และคอลเล็กชัน workout_logs | ปลายทางและสคีมา | ทีม |
| 2026-09-04 | เพิ่มสคีมาหน่วยความจำ 3 ระดับของ user_profiles และอัปเดต POST /api/coach/chat ด้วย memory_mutations | ปลายทางและสคีมา | ทีม |
| 2026-09-04 | ส่วนที่ 1 (ขอบเขตและหลักการของระบบ) และส่วนที่ 4 (วงจรและไปป์ไลน์ข้อมูล) เสร็จสมบูรณ์แล้ว | การดูจนจบแบบเต็มหน้าจอ | ทีม |
| 2026-09-22 | ข้อกำหนดที่สอดคล้องกัน: สแต็ก Next.js + Tailwind + Zustand, การตรวจสอบสิทธิ์ Bearer, คอลเล็กชันย่อย PR, กฎ Firestore และ Slug แบบฝึกหัด Canonical | การปรับข้อกำหนดทั้งหมดให้สอดคล้องกัน | การตรวจสอบสถาปัตยกรรม |