在「來企排隊」系統中,我們整合了 Google 地圖功能,讓用戶可以:
商店列表頁面 → 點擊地圖按鈕 → 開啟 Google 地圖彈窗
親愛的 [商家名稱]:
[用戶姓名] 傳送預約邀請,請登入 來企排隊 官方網站,查看詳細資訊。
# 在 Google Cloud Console 中
1. 前往「APIs & Services」→「Credentials」
2. 點擊「Create Credentials」→「API Key」
3. 複製產生的 API 金鑰
4. 設定 API 金鑰限制(限制網域和 API)
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your-google-maps-api-key
NEXT_PUBLIC_GOOGLE_MAPS_ID=your-google-maps-id
// components/GoogleMapModal.tsx
export default function GoogleMapModal({ isOpen, onClose }) {
useEffect(() => {
if (!isOpen) return;
const initMap = () => {
if (!window.google || !mapRef.current) return;
// 取得用戶位置
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
async (position) => {
const userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
// 建立地圖實例
const newMapInstance = new window.google.maps.Map(mapRef.current, {
center: userLocation,
zoom: 15,
mapId: process.env.NEXT_PUBLIC_GOOGLE_MAPS_ID,
});
setMapInstance(newMapInstance);
await searchNearbyPlaces(userLocation, newMapInstance);
},
(error) => {
setError("無法取得您的位置,請檢查位置權限設定");
setIsLoading(false);
}
);
}
};
// 載入 Google Maps API
if (!window.google) {
const script = document.createElement("script");
script.src = `https://maps.googleapis.com/maps/api/js?key=${process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY}&libraries=places,marker&loading=async&callback=initGoogleMap`;
script.async = true;
script.defer = true;
window.initGoogleMap = initMap;
document.head.appendChild(script);
} else {
initMap();
}
}, [isOpen]);
return (
<div>
...
<div ref={mapRef} />
...
</div>
);
}
const searchNearbyPlaces = async (userLocation, mapInstance) => {
try {
// 檢查 Places API 是否可用
if (!window.google.maps.places?.Place?.searchNearby) {
setIsLoading(false);
return;
}
const request = {
includedTypes: ["store", "restaurant"],
locationRestriction: {
center: userLocation,
radius: 1000, // 1公里範圍
},
fields: ["id", "displayName", "location", "formattedAddress"],
};
const { places } = await window.google.maps.places.Place.searchNearby(request);
if (places && places.length > 0) {
places.forEach((place) => createMarker(place, mapInstance));
}
} catch (err) {
console.error("Places search failed:", err);
} finally {
setIsLoading(false);
}
};
-- supabase/migrations/create_sms_invitations_table.sql
CREATE TABLE IF NOT EXISTS public.sms_invitations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
-- 發送者資訊
sender_user_id UUID REFERENCES public.users(id) ON DELETE CASCADE,
sender_name TEXT NOT NULL,
sender_phone TEXT,
-- 商家資訊
store_phone TEXT NOT NULL, -- 商家手機號碼
store_name TEXT NOT NULL, -- 從地圖獲取的商家名稱
store_place_id TEXT, -- Google Places ID
-- 邀請狀態
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'converted', 'expired')),
booking_id UUID REFERENCES public.bookings(id) ON DELETE SET NULL,
expires_at TIMESTAMPTZ DEFAULT (now() + INTERVAL '7 days') NOT NULL,
message_content TEXT
);
-- 創建唯一索引:只對 pending 狀態保持唯一性
CREATE UNIQUE INDEX sms_invitations_pending_unique
ON public.sms_invitations (sender_user_id, store_phone)
WHERE status = 'pending';
-- 創建簡訊邀請的資料庫函數
CREATE OR REPLACE FUNCTION create_sms_invitation(
p_store_phone TEXT,
p_store_name TEXT,
p_store_place_id TEXT DEFAULT NULL,
p_message_content TEXT DEFAULT NULL
)
RETURNS TABLE (
invitation_id UUID,
message TEXT
)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_user_id UUID;
v_user_name TEXT;
v_invitation_id UUID;
v_default_message TEXT;
BEGIN
-- 獲取當前用戶資訊
SELECT auth.uid() INTO v_user_id;
IF v_user_id IS NULL THEN
RAISE EXCEPTION 'User must be authenticated';
END IF;
-- 獲取用戶姓名
SELECT name INTO v_user_name FROM public.users WHERE id = v_user_id;
-- 生成預設簡訊內容
v_default_message := format(
'親愛的 %s:%s 傳送預約邀請,請登入 來企排隊 官方網站,查看詳細資訊。',
p_store_name,
v_user_name
);
-- 創建邀請記錄
INSERT INTO public.sms_invitations (
sender_user_id, sender_name, store_phone, store_name,
store_place_id, message_content
) VALUES (
v_user_id, v_user_name, p_store_phone, p_store_name,
p_store_place_id, COALESCE(p_message_content, v_default_message)
) RETURNING id INTO v_invitation_id;
RETURN QUERY SELECT v_invitation_id, v_default_message;
END;
$$;
const handleSendSMS = async (phoneNumber: string, businessName: string) => {
try {
// 創建邀請記錄
const { data, error } = await supabase.rpc('create_sms_invitation', {
p_store_phone: phoneNumber,
p_store_name: businessName,
p_store_place_id: null,
p_message_content: null // 使用預設訊息
});
// 獲取用戶資訊
const { data: { user } } = await supabase.auth.getUser();
const { data: userProfile } = await supabase
.from('users')
.select('name')
.eq('id', user?.id)
.single();
const userName = userProfile?.name || '用戶';
// 發送簡訊
const message = data?.message || `親愛的 ${businessName}:${userName} 傳送預約邀請,請登入 來企排隊 官方網站,查看詳細資訊。`;
const smsLink = `sms:${phoneNumber}?body=${encodeURIComponent(message)}`;
window.location.href = smsLink;
toast.success('預約邀請已發送!商家確認後您將收到通知。');
} catch (err) {
console.error('Error sending SMS invitation:', err);
}
};
透過地圖操作,用戶可以發現新商家並邀請加入,而商家也能透過這個機制了解並使用服務。
... to be continued
有任何想討論歡迎留言,或需要指正的地方請鞭大力一點,歡迎訂閱、按讚加分享,分享給想要提升開發效率的朋友