iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0

前言:當 100 個跑者同時向 AI 許願時

過去的 22 天,我們單人用戶玩得很開心,功能也都順利的進行,但當 KAKERU 實際上線,100 個使用者同時點擊「生成課表」時(哪來的自信這麼多?!),後端該怎麼區分誰是誰?

今天要引入一個方法 Local-First 架構,利用 UUID 讓手機在背景配發一組全球唯一的身分證。同時,將原本死板的 4 週課表升級為根據目標賽事日期自動推算的 24 週動態備賽週期引擎!

觀念解說:UUID 匿名隔離、動態週期與體驗細節

  1. 免登入的極意 (UUID): 在跑者初次打開 APP 時,前端自動生成一組全球唯一的字串(如 user_8f4c2...),並透過 Zustand Persist 持久化儲存在手機硬碟中。UI 畫面上完全不干擾使用者(無須顯示冷冰冰的 UUID),但發給後端的每一次 API 請求都會默默帶上。
  2. Firestore 的對應: Firestore 不在乎你有沒有註冊帳號,它只認 Document ID。我們直接拿這組 UUID 當作文件隔離識別碼,如此一來,100 萬個使用者就有 100 萬份互不干擾的課表與對話記憶。
  3. 動態備賽週期引擎 (4 ~ 24 週): 真實的馬拉松備賽動輒 16 到 24 週。跑者在生成課表時選擇「目標賽事日期」,前端動態計算出距離今天的週數,並將數字餵給後端 Spring Boot 與 Gemini,依據時間長度編排「基礎期 ➔ 強化期 ➔ 巔峰期 ➔ 減量期」的週期化訓練(Periodization)。
  4. 介面職責分離與智慧打卡
    • 個人檔案 (ProfileScreen):專注於暱稱、身高、體重與 BMI 評估。
    • 課表嚮導 (ScheduleScreen):未生成課表前提供目標賽事與日期選擇;生成後專注於課表執行。
    • 智慧打卡分流 (DailyWorkoutList):跑步課表呼叫 AI 跑錶截圖辨識;遇到「完全休息」或「肌力訓練」時提供專屬的一鍵完成打卡,體驗更自然順暢!

動手實作 1:前端生成並持久化 UUID

為了讓 APP 初次開啟時自動配發唯一識別碼,我們使用 react-native-uuid 套件,並在 Zustand Store 初始化的瞬間生成 uuid.v4()。透過 persist 中介軟體,這個 ID 會被存進手機的 AsyncStorage 中,即便跑者滑掉 APP 或重開機,這組身分證也永遠不會改變。

安裝所需套件:

npx expo install react-native-uuid @react-native-community/datetimepicker

在 Zustand Store 初始化並持久化 userId

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
import uuid from 'react-native-uuid';

interface AppState {
  userId: string;
  selectedWeek: number;
  // ... 其他狀態定義
}

export const useStore = create<AppState>()(
  persist(
    (set) => ({
      // 如果還沒有 userId,就在初始化的瞬間配發一個全球唯一的 UUID
      userId: uuid.v4() as string,
      selectedWeek: 1,

      // ... 下半部 Actions 與 Reducer 實作省略
    }),
    {
      name: 'kakeru-storage',
      storage: createJSONStorage(() => AsyncStorage),
      // ... 下半部持久化設定省略
    }
  )
);

如此一來,前端每次發起 API 請求時,只需從 useStore 取出 userId 放到 Payload 裡,後端的 Firestore 就能精準識別跑者,達成真正的免登入多用戶隔離。
https://ithelp.ithome.com.tw/upload/images/20260901/201650438oA92jBKJZ.png

動手實作 2:課表生成嚮導與 4~24 週動態推算

進入課表生成流程時,我們導入 @react-native-community/datetimepicker 讓跑者挑選預計參賽的日期。接著透過時間差計算公式算出距離今日的週數,並設定最少 4 週(急行軍模式)、最多 24 週(完整半年週期)的彈性防呆限制。

在 ScheduleScreen 中加入賽事日期選擇與動態週數計算:

// src/app/schedule.tsx (節錄)
export default function ScheduleScreen() {
  const [raceDate, setRaceDate] = useState(new Date(Date.now() + 120 * 24 * 60 * 60 * 1000));

  // 計算距離今天還有幾週 (最少 4 週,最多 24 週)
  const calculateTotalWeeks = (targetDate: Date) => {
    const today = new Date();
    const diffTime = Math.max(0, targetDate.getTime() - today.getTime());
    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
    const weeks = Math.ceil(diffDays / 7);
    return Math.min(Math.max(weeks, 4), 24);
  };

  const totalWeeks = calculateTotalWeeks(raceDate);

  // ... 下半部表單與生成 API 呼叫省略
}

透過將算出的 totalWeeks 傳送給後端,AI 教練便能知道這次備賽到底有多少時間可以安排,避免出現「下個月要比賽卻給了 20 週課表」的尷尬情況。

https://ithelp.ithome.com.tw/upload/images/20260901/20165043yAqtHsx3TR.png

動手實作 3:週次切換器 WeekSelector 與自動滾動對焦

當課表長達 24 週時,按鈕會超出螢幕寬度。我們將 WeekSelector 升級為橫向可滑動的 ScrollView,並利用 React 的 useRef 與 useEffect 監聽目前選取的週次,當跑者點選不同週次時,按鈕列會自動平滑滾動到可視正中央。

升級可滑動與自動對焦的週次選擇元件:

// src/components/WeekSelector.tsx (節錄)
import React, { useRef, useEffect } from 'react';
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useStore } from '../store/useStore';

export function WeekSelector({ totalWeeks = 4 }: { totalWeeks?: number }) {
  const scrollViewRef = useRef<ScrollView>(null);
  const selectedWeek = useStore((state) => state.selectedWeek);
  const setSelectedWeek = useStore((state) => state.setSelectedWeek);

  const weeksArray = Array.from({ length: totalWeeks }, (_, i) => i + 1);

  // 選取週次時,自動平滑滾動至可視區域
  useEffect(() => {
    if (scrollViewRef.current && selectedWeek > 0) {
      scrollViewRef.current.scrollTo({ x: Math.max(0, (selectedWeek - 2) * 90), animated: true });
    }
  }, [selectedWeek]);

  // ... 下半部 JSX 渲染與 Stylesheet 樣式省略
}

頂部保留「第 X 週 / 共 Y 週」的進度提示,讓跑者隨時掌握目前在整個半年大週期中的訓練位置。

https://ithelp.ithome.com.tw/upload/images/20260901/20165043BFn03ehEuH.jpg

動手實作 4:後端 Spring Boot Prompt 動態改寫與用戶隔離

前端把 userId 與 totalWeeks 傳到後端後,Spring Boot 的 Gemini Prompt 必須變得更有彈性,不能再寫死 4 週了!我們改寫 Prompt 範本,動態將週數注入提示詞,要求 Gemini 依時間長度嚴格進行週期化訓練規劃。

修改 Prompt 範本 (generate-marathon.md):

<!-- prompts/generate-marathon.md (節錄) -->
你是一位嚴格且專業的馬拉松教練。
評估學員資料:
- 目標賽事:{{TARGET_RACE}}
- 備賽週期:{{TOTAL_WEEKS}} 週
- 目前體能:{{CURRENT_LEVEL}}

【週期化訓練規劃】
請根據學員的備賽長度(共 {{TOTAL_WEEKS}} 週),嚴格進行「週期化訓練 (Periodization)」編排(依序規劃:基礎期 -> 強化期 -> 巔峰期 -> 減量期)。
JSON 的 scheduleData 必須包含長度為 {{TOTAL_WEEKS}} 週的完整每日訓練課表(weekNumber: 1 至 {{TOTAL_WEEKS}})。

<!-- ... 下半部 JSON 欄位規範與領域防禦守則省略 -->

在 Java 服務層動態組合 Prompt 並隔離儲存歷史:

@Override
public GenerateScheduleResponseDto generateSchedule(GenerateScheduleRequest request) {
    int totalWeeks = (request.getTotalWeeks() != null && request.getTotalWeeks() > 0)
            ? Math.min(Math.max(request.getTotalWeeks(), 4), 24)
            : 4;

    String prompt = template
            .replace("{{TARGET_RACE}}", request.getTargetRace() != null ? request.getTargetRace() : "")
            .replace("{{TOTAL_WEEKS}}", String.valueOf(totalWeeks))
            .replace("{{CURRENT_LEVEL}}", request.getCurrentLevel() != null ? request.getCurrentLevel() : "");

    String rawJson = callGeminiApiWithFallback(List.of(new GeminiRequest.Part(prompt)));
    String cleanedJson = cleanJsonResponse(rawJson);

    // 儲存對話歷史時帶入 userId 達成資料庫多租戶隔離
    saveHistory(request.getUserId(), request.getSessionId(), prompt, cleanedJson);

    // ... 下半部 JSON 反序列化與 Guardrail 驗證省略
}

這樣一來,不論跑者是下個月就要上陣(急行軍 4 週),還是半年後才比賽(完整週期 24 週),Gemini 都能依據時長產出最合適的每週配速與跑量堆疊計畫。
https://ithelp.ithome.com.tw/upload/images/20260901/20165043FY0kknSHho.jpg

今日總結與明日預告

今天在友克鑫市完成了 KAKERU 系統底層的升級!

  1. Local-First 多用戶隔離:透過一行簡單的 uuid.v4(),省去了幾百行枯燥的登入/註冊 UI 與 Token 刷新邏輯,完美實現了零門檻的多用戶隔離。
  2. 24 週動態備賽引擎:讓 AI 教練不再死板排定 4 週,而是能依據目標賽事日期量身打造長達半年的週期化課表。
  3. 自動對焦切換器:解決了長週期課表在手機小螢幕上的瀏覽痛點。

目前課表上顯示的「第 1 週 週一」太過抽象。明天(Day 24)我們將實作「真實日曆映射系統」,將相對週次精準轉換為實際日期(例如:2026/08/31),並整合 Day 22 的 AI 視覺解析與智慧打卡分流,打造專屬跑者的運動日記!


上一篇
Day 22 | 繞過 API 授權地獄,用 Gemini 多模態看懂跑錶與跑步機螢幕
下一篇
Day 24 | 所以今天是幾月幾號?告別「第 X 週」,日曆對齊與打卡實戰
系列文
單鐵的人生如履薄冰!AI 教練 APP 30天開發旅程,你說能走到最後嗎?24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言