taiwan-7-11 / app.py
tbdavid2019's picture
提示地址可留白
0b75bed
import gradio as gr
import requests
import json
import os
import pandas as pd
from geopy.distance import geodesic
# =============== 7-11 所需常數 ===============
# 請確認此處的 MID_V 是否有效,若過期請更新
MID_V = "W0_DiF4DlgU5OeQoRswrRcaaNHMWOL7K3ra3385ocZcv-bBOWySZvoUtH6j-7pjiccl0C5h30uRUNbJXsABCKMqiekSb7tdiBNdVq8Ro5jgk6sgvhZla5iV0H3-8dZfASc7AhEm85679LIK3hxN7Sam6D0LAnYK9Lb0DZhn7xeTeksB4IsBx4Msr_VI"
USER_AGENT_7_11 = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
API_7_11_BASE = "https://lovefood.openpoint.com.tw/LoveFood/api"
# =============== FamilyMart 所需常數 ===============
FAMILY_PROJECT_CODE = "202106302" # 若有需要請自行調整
API_FAMILY = "https://stamp.family.com.tw/api/maps/MapProductInfo"
def get_7_11_token():
url = f"{API_7_11_BASE}/Auth/FrontendAuth/AccessToken?mid_v={MID_V}"
headers = {"user-agent": USER_AGENT_7_11}
resp = requests.post(url, headers=headers, data="")
resp.raise_for_status()
js = resp.json()
if not js.get("isSuccess"):
raise RuntimeError(f"取得 7-11 token 失敗: {js}")
return js["element"]
def get_7_11_nearby_stores(token, lat, lon):
url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetNearbyStoreList?token={token}"
headers = {
"user-agent": USER_AGENT_7_11,
"content-type": "application/json",
}
body = {
"CurrentLocation": {"Latitude": lat, "Longitude": lon},
"SearchLocation": {"Latitude": lat, "Longitude": lon}
}
resp = requests.post(url, headers=headers, json=body)
resp.raise_for_status()
js = resp.json()
if not js.get("isSuccess"):
raise RuntimeError(f"取得 7-11 附近門市失敗: {js}")
return js["element"].get("StoreStockItemList", [])
def get_7_11_store_detail(token, lat, lon, store_no):
url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetStoreDetail?token={token}"
headers = {
"user-agent": USER_AGENT_7_11,
"content-type": "application/json",
}
body = {
"CurrentLocation": {"Latitude": lat, "Longitude": lon},
"StoreNo": store_no
}
resp = requests.post(url, headers=headers, json=body)
resp.raise_for_status()
js = resp.json()
if not js.get("isSuccess"):
raise RuntimeError(f"取得 7-11 門市({store_no})資料失敗: {js}")
return js["element"].get("StoreStockItem", {})
def get_family_nearby_stores(lat, lon):
headers = {"Content-Type": "application/json;charset=utf-8"}
body = {
"ProjectCode": FAMILY_PROJECT_CODE,
"latitude": lat,
"longitude": lon
}
resp = requests.post(API_FAMILY, headers=headers, json=body)
resp.raise_for_status()
js = resp.json()
if js.get("code") != 1:
raise RuntimeError(f"取得全家門市資料失敗: {js}")
return js["data"]
def find_nearest_store(address, lat, lon, distance_km):
"""
distance_km: 從下拉選單取得的「公里」(字串),例如 '3' or '5' ...
"""
print(f"🔍 收到查詢請求: address={address}, lat={lat}, lon={lon}, distance_km={distance_km}")
# 若有填地址但 lat/lon 為 0,嘗試用 Google Geocoding API
if address and address.strip() != "" and (lat == 0 or lon == 0):
try:
import requests
import os
googlekey = os.environ.get("googlekey")
if not googlekey:
raise RuntimeError("未設定 googlekey,請於 Huggingface Space Secrets 設定。")
geocode_url = "https://maps.googleapis.com/maps/api/geocode/json"
params = {
"address": address,
"key": googlekey
}
resp = requests.get(geocode_url, params=params)
resp.raise_for_status()
data = resp.json()
if data.get("status") == "OK" and data.get("results"):
location = data["results"][0]["geometry"]["location"]
lat = float(location["lat"])
lon = float(location["lng"])
print(f"地址轉換成功: {address} => lat={lat}, lon={lon}")
else:
print(f"❌ Google Geocoding 失敗: {data}")
return [["❌ 地址轉換失敗,請輸入正確地址", "", "", "", ""]], 0, 0
except Exception as e:
print(f"❌ Google Geocoding 失敗: {e}")
return [["❌ 地址轉換失敗,請輸入正確地址", "", "", "", ""]], 0, 0
if lat == 0 or lon == 0:
return [["❌ 請輸入地址或提供 GPS 座標", "", "", "", ""]], lat, lon
# 將 km 轉成公尺
max_distance = float(distance_km) * 1000
result_rows = []
# ------------------ 7-11 ------------------
try:
token_711 = get_7_11_token()
nearby_stores_711 = get_7_11_nearby_stores(token_711, lat, lon)
for store in nearby_stores_711:
dist_m = store.get("Distance", 999999)
if dist_m <= max_distance:
store_no = store.get("StoreNo")
store_name = store.get("StoreName", "7-11 未提供店名")
remaining_qty = store.get("RemainingQty", 0)
if remaining_qty > 0:
detail = get_7_11_store_detail(token_711, lat, lon, store_no)
for cat in detail.get("CategoryStockItems", []):
cat_name = cat.get("Name", "")
for item in cat.get("ItemList", []):
item_name = item.get("ItemName", "")
item_qty = item.get("RemainingQty", 0)
row = [
f"7-11 {store_name}",
f"{dist_m:.1f} m",
f"{cat_name} - {item_name}",
str(item_qty),
dist_m # 用來排序
]
result_rows.append(row)
else:
row = [
f"7-11 {store_name}",
f"{dist_m:.1f} m",
"即期品 0 項",
"0",
dist_m
]
result_rows.append(row)
except Exception as e:
print(f"❌ 取得 7-11 即期品時發生錯誤: {e}")
# ------------------ FamilyMart ------------------
try:
nearby_stores_family = get_family_nearby_stores(lat, lon)
for store in nearby_stores_family:
dist_m = store.get("distance", 999999)
if dist_m <= max_distance:
store_name = store.get("name", "全家 未提供店名")
info_list = store.get("info", [])
has_item = False
for big_cat in info_list:
big_cat_name = big_cat.get("name", "")
for subcat in big_cat.get("categories", []):
subcat_name = subcat.get("name", "")
for product in subcat.get("products", []):
product_name = product.get("name", "")
qty = product.get("qty", 0)
if qty > 0:
has_item = True
row = [
f"全家 {store_name}",
f"{dist_m:.1f} m",
f"{big_cat_name} - {subcat_name} - {product_name}",
str(qty),
dist_m
]
result_rows.append(row)
if not has_item:
row = [
f"全家 {store_name}",
f"{dist_m:.1f} m",
"即期品 0 項",
"0",
dist_m
]
result_rows.append(row)
except Exception as e:
print(f"❌ 取得全家 即期品時發生錯誤: {e}")
if not result_rows:
return [["❌ 附近沒有即期食品 (在所選公里範圍內)", "", "", "", ""]], lat, lon
# 排序:依照最後一欄 (float 距離) 做由小到大排序
result_rows.sort(key=lambda x: x[4])
# 移除最後一欄 (不顯示給前端)
for row in result_rows:
row.pop()
return result_rows, lat, lon
# ========== Gradio 介面 ==========
import gradio as gr
def main():
with gr.Blocks() as demo:
gr.Markdown("## 台灣7-11 和 family全家便利商店「即期食品」 乞丐時光搜尋")
gr.Markdown("""
1. 按下「📍🔍 自動定位並搜尋」可自動取得目前位置並直接查詢附近即期品
2. 也可手動輸入地址、緯度、經度與搜尋範圍後再按此按鈕
3. 意見反應 telegram @a7a8a9abc
""")
address = gr.Textbox(label="地址(可留空)", placeholder="可留空白,通常不用填")
lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
# 下拉選單,提供可選距離 (公里)
distance_dropdown = gr.Dropdown(
label="搜尋範圍 (公里)",
choices=["3", "5", "7", "13", "21"],
value="3", # 預設 3 公里
interactive=True
)
with gr.Row():
auto_gps_search_button = gr.Button("📍🔍 自動定位並搜尋", elem_id="auto-gps-search-btn")
output_table = gr.Dataframe(
headers=["門市", "距離 (m)", "商品/即期食品", "數量"],
interactive=False
)
# 只保留自動定位並搜尋按鈕
# (已移除 gps_button)
# 新增自動定位並搜尋按鈕
# auto_gps_search_button.click(
# fn=find_nearest_store,
# inputs=[address, lat, lon, distance_dropdown],
# outputs=output_table,
# js="""
# (address, lat, lon, distance) => {
# return new Promise((resolve) => {
# if (!navigator.geolocation) {
# alert("您的瀏覽器不支援地理位置功能");
# resolve([address, 0, 0, distance]);
# return;
# }
# navigator.geolocation.getCurrentPosition(
# (position) => {
# resolve([address, position.coords.latitude, position.coords.longitude, distance]);
# },
# (error) => {
# alert("無法取得位置:" + error.message);
# resolve([address, 0, 0, distance]);
# }
# );
# });
# }
# """
# )
# 修正版:自動定位並搜尋,查詢同時回填 lat/lon 欄位,address 有填時不抓 GPS
auto_gps_search_button.click(
fn=find_nearest_store,
inputs=[address, lat, lon, distance_dropdown],
outputs=[output_table, lat, lon],
js="""
(address, lat, lon, distance) => {
function isZero(val) {
return !val || Number(val) === 0;
}
if (address && address.trim() !== "") {
// 有填地址,直接查詢,不抓 GPS
return [address, Number(lat), Number(lon), distance, Number(lat), Number(lon)];
}
if (!isZero(lat) && !isZero(lon)) {
// 沒填地址但有座標,直接查詢
return [address, Number(lat), Number(lon), distance, Number(lat), Number(lon)];
}
// 沒填地址且沒座標,抓 GPS
return new Promise((resolve) => {
if (!navigator.geolocation) {
alert("您的瀏覽器不支援地理位置功能");
resolve([address, 0, 0, distance, 0, 0]);
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const newLat = position.coords.latitude;
const newLon = position.coords.longitude;
resolve([address, newLat, newLon, distance, newLat, newLon]);
},
(error) => {
alert("無法取得位置:" + error.message);
resolve([address, 0, 0, distance, 0, 0]);
}
);
});
}
"""
)
demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
if __name__ == "__main__":
main()