tbdavid2019 commited on
Commit
b0a371d
·
1 Parent(s): 5c3d1cd

增加範圍公里可選

Browse files
Files changed (3) hide show
  1. README.md +70 -0
  2. app.py +87 -95
  3. app3.py +255 -0
README.md CHANGED
@@ -11,3 +11,73 @@ short_description: 便利商店的打折品
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
14
+
15
+
16
+ ---
17
+
18
+ # 便利商店即期食品查詢 (7-11 / FamilyMart)
19
+
20
+ 本專案透過非官方 API,查詢台灣 7-11 與全家便利商店「即期食品」的資訊。
21
+
22
+ > **注意:**
23
+ > 1. 需要有效的 `MID_V` 值才能查詢 7-11 資料。`MID_V` 可能會隨官方更新而失效,需自行抓包或取得授權。
24
+ > 2. 全家 API 可能會變動或停止。
25
+ > 3. 本專案僅供研究教學之用,請勿用於商業或非法目的。
26
+
27
+ ## 功能
28
+ - 以 GPS 座標或地址(目前僅示意)搜尋附近的 7-11 / 全家門市。
29
+ - 可自訂搜尋範圍(3 / 5 / 7 / 13 / 21 公里)。
30
+ - 顯示每間門市的即期食品清單與剩餘數量。
31
+
32
+ ## 使用方式
33
+ 1. 安裝 Python 3.8+ 及套件:
34
+ ```bash
35
+ pip install gradio requests pandas geopy
36
+
37
+ 2. 執行:
38
+
39
+ python app.py
40
+
41
+
42
+ 3. 瀏覽器打開 http://127.0.0.1:7860,即可看到查詢介面。
43
+
44
+ 注意事項
45
+ • 此為個人練習與技術示範,非官方專案。
46
+ • 若出現「憑證過期」或「Token 失敗」等訊息,表示 MID_V 失效,需要更新。
47
+
48
+ Convenience Store Expiring-Food Query (7-11 / FamilyMart)
49
+
50
+ This project uses unofficial APIs to query expiring-food items in Taiwan’s 7-11 and FamilyMart convenience stores.
51
+
52
+ Note:
53
+ 1. A valid MID_V is required to access 7-11’s data. MID_V may expire as the official app updates. You must capture or obtain it by yourself.
54
+ 2. FamilyMart’s API might change or be discontinued without notice.
55
+ 3. This project is for educational and research purposes only. Please do not use it for commercial or illegal purposes.
56
+
57
+ Features
58
+ • Search nearby 7-11 / FamilyMart stores by GPS coordinates.
59
+ • Customizable search radius (3 / 5 / 7 / 13 / 21 km).
60
+ • Display each store’s expiring-food items and remaining quantity.
61
+
62
+ Usage
63
+ 1. Install Python 3.8+ and dependencies:
64
+
65
+ pip install gradio requests pandas geopy
66
+
67
+
68
+ 2. Run:
69
+
70
+ python app.py
71
+
72
+
73
+ 3. Open http://127.0.0.1:7860 in your browser to see the interface.
74
+
75
+ Disclaimer
76
+ • This is a personal practice/demo project, not affiliated with or endorsed by 7-11 or FamilyMart.
77
+ • If you see “certificate expired” or “token error,” it means MID_V is invalid and must be updated.
78
+
79
+ > - 你可以將上述內容直接複製到 `README.md` 放到 GitHub。
80
+ > - 若有更多需求,可自行在 README 中補充專案背景、技術細節、授權條款等。
81
+
82
+ ---
83
+
app.py CHANGED
@@ -7,7 +7,7 @@ from geopy.distance import geodesic
7
 
8
  # =============== 7-11 所需常數 ===============
9
  # 請確認此處的 MID_V 是否有效,若過期請更新
10
- MID_V = "W0_DiF4DlgU5OeQoRswrRcaaNHMWOL7K3ra3385ocZcv-bBOWySZvoUtH6j-7pjiccl0C5h30uRUNbJXsABCKMqiekSb7tdiBNdVq8Ro5jgk6sgvhZla5iV0H3-8dZfASc7AhEm85679LIK3hxN7Sam6D0LAnYK9Lb0DZhn7xeTeksB4IsBx4Msr_VI" # 請填入有效的 mid_v
11
  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"
12
  API_7_11_BASE = "https://lovefood.openpoint.com.tw/LoveFood/api"
13
 
@@ -15,28 +15,17 @@ API_7_11_BASE = "https://lovefood.openpoint.com.tw/LoveFood/api"
15
  FAMILY_PROJECT_CODE = "202106302" # 若有需要請自行調整
16
  API_FAMILY = "https://stamp.family.com.tw/api/maps/MapProductInfo"
17
 
18
- # 3 公里範圍
19
- MAX_DISTANCE = 3000
20
 
21
- # -----------------------------------------------------------
22
- # 7-11: 取得 AccessToken
23
- # -----------------------------------------------------------
24
  def get_7_11_token():
25
  url = f"{API_7_11_BASE}/Auth/FrontendAuth/AccessToken?mid_v={MID_V}"
26
- headers = {
27
- "user-agent": USER_AGENT_7_11
28
- }
29
  resp = requests.post(url, headers=headers, data="")
30
  resp.raise_for_status()
31
  js = resp.json()
32
  if not js.get("isSuccess"):
33
  raise RuntimeError(f"取得 7-11 token 失敗: {js}")
34
- token = js["element"]
35
- return token
36
 
37
- # -----------------------------------------------------------
38
- # 7-11: 取得附近門市清單 (含剩餘即期品總數量)
39
- # -----------------------------------------------------------
40
  def get_7_11_nearby_stores(token, lat, lon):
41
  url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetNearbyStoreList?token={token}"
42
  headers = {
@@ -54,9 +43,6 @@ def get_7_11_nearby_stores(token, lat, lon):
54
  raise RuntimeError(f"取得 7-11 附近門市失敗: {js}")
55
  return js["element"].get("StoreStockItemList", [])
56
 
57
- # -----------------------------------------------------------
58
- # 7-11: 取得單一門市的即期品清單
59
- # -----------------------------------------------------------
60
  def get_7_11_store_detail(token, lat, lon, store_no):
61
  url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetStoreDetail?token={token}"
62
  headers = {
@@ -74,13 +60,8 @@ def get_7_11_store_detail(token, lat, lon, store_no):
74
  raise RuntimeError(f"取得 7-11 門市({store_no})資料失敗: {js}")
75
  return js["element"].get("StoreStockItem", {})
76
 
77
- # -----------------------------------------------------------
78
- # FamilyMart: 取得附近門市即期品清單 (單次呼叫可拿到所有商品細項)
79
- # -----------------------------------------------------------
80
  def get_family_nearby_stores(lat, lon):
81
- headers = {
82
- "Content-Type": "application/json;charset=utf-8",
83
- }
84
  body = {
85
  "ProjectCode": FAMILY_PROJECT_CODE,
86
  "latitude": lat,
@@ -89,19 +70,21 @@ def get_family_nearby_stores(lat, lon):
89
  resp = requests.post(API_FAMILY, headers=headers, json=body)
90
  resp.raise_for_status()
91
  js = resp.json()
92
- # 根據回傳範例,成功時 code 為 1
93
  if js.get("code") != 1:
94
  raise RuntimeError(f"取得全家門市資料失敗: {js}")
95
  return js["data"]
96
 
97
- # -----------------------------------------------------------
98
- # Gradio 查詢邏輯
99
- # -----------------------------------------------------------
100
- def find_nearest_store(address, lat, lon):
101
- print(f"🔍 收到查詢請求: address={address}, lat={lat}, lon={lon}")
102
  if lat == 0 or lon == 0:
103
  return [["❌ 請輸入地址或提供 GPS 座標", "", "", "", ""]]
104
 
 
 
 
105
  result_rows = []
106
 
107
  # ------------------ 7-11 ------------------
@@ -110,7 +93,7 @@ def find_nearest_store(address, lat, lon):
110
  nearby_stores_711 = get_7_11_nearby_stores(token_711, lat, lon)
111
  for store in nearby_stores_711:
112
  dist_m = store.get("Distance", 999999)
113
- if dist_m <= MAX_DISTANCE:
114
  store_no = store.get("StoreNo")
115
  store_name = store.get("StoreName", "7-11 未提供店名")
116
  remaining_qty = store.get("RemainingQty", 0)
@@ -121,13 +104,12 @@ def find_nearest_store(address, lat, lon):
121
  for item in cat.get("ItemList", []):
122
  item_name = item.get("ItemName", "")
123
  item_qty = item.get("RemainingQty", 0)
124
- # 在最後加一個 float 距離欄位以便排序
125
  row = [
126
  f"7-11 {store_name}",
127
  f"{dist_m:.1f} m",
128
  f"{cat_name} - {item_name}",
129
  str(item_qty),
130
- dist_m # 供排序用
131
  ]
132
  result_rows.append(row)
133
  else:
@@ -136,7 +118,7 @@ def find_nearest_store(address, lat, lon):
136
  f"{dist_m:.1f} m",
137
  "即期品 0 項",
138
  "0",
139
- dist_m # 供排序用
140
  ]
141
  result_rows.append(row)
142
  except Exception as e:
@@ -147,7 +129,7 @@ def find_nearest_store(address, lat, lon):
147
  nearby_stores_family = get_family_nearby_stores(lat, lon)
148
  for store in nearby_stores_family:
149
  dist_m = store.get("distance", 999999)
150
- if dist_m <= MAX_DISTANCE:
151
  store_name = store.get("name", "全家 未提供店名")
152
  info_list = store.get("info", [])
153
  has_item = False
@@ -165,7 +147,7 @@ def find_nearest_store(address, lat, lon):
165
  f"{dist_m:.1f} m",
166
  f"{big_cat_name} - {subcat_name} - {product_name}",
167
  str(qty),
168
- dist_m # 供排序用
169
  ]
170
  result_rows.append(row)
171
  if not has_item:
@@ -174,82 +156,92 @@ def find_nearest_store(address, lat, lon):
174
  f"{dist_m:.1f} m",
175
  "即期品 0 項",
176
  "0",
177
- dist_m # 供排序用
178
  ]
179
  result_rows.append(row)
180
  except Exception as e:
181
  print(f"❌ 取得全家 即期品時發生錯誤: {e}")
182
 
183
  if not result_rows:
184
- return [["❌ 附近 3 公里內沒有即期食品", "", "", "", ""]]
185
 
186
- # ============= 在這裡進行排序 =============
187
- # result_rows 的結構是 [門市, 距離(字串), 商品, 數量, float_distance]
188
- # 我們要依照最後一欄 float_distance 做由小到大排序
189
  result_rows.sort(key=lambda x: x[4])
190
-
191
- # 排序完之後,再把最後一欄刪掉 (不顯示給使用者)
192
  for row in result_rows:
193
- row.pop() # 移除 index=4 (float_distance)
194
 
195
  return result_rows
196
 
197
- # -----------------------------------------------------------
198
- # Gradio 介面
199
- # -----------------------------------------------------------
200
  import gradio as gr
201
 
202
- with gr.Blocks() as demo:
203
- gr.Markdown("## 便利商店「即期食品」搜尋示範")
204
- gr.Markdown("""
205
- 1. 按下「使用目前位置」或自行輸入緯度/經度
206
- 2. 點選「搜尋」查詢 3 公里內 7-11 / 全家的即期品
207
- 3. 若要執行,需要有效的 mid_v (7-11 愛食記憶官網)
208
- 4. Logs 查看詳細錯誤或除錯資訊
209
- """)
210
- address = gr.Textbox(label="輸入地址(可留空)")
211
- lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
212
- lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
213
-
214
- with gr.Row():
215
- gps_button = gr.Button("📍 ❶ 使用目前位置-先按這個 並等待3秒 ", elem_id="gps-btn")
216
- search_button = gr.Button("🔍 ❷ 搜尋 ")
217
-
218
- output_table = gr.Dataframe(
219
- headers=["門市", "距離 (m)", "商品/即期食品", "數量"],
220
- interactive=False
221
- )
222
-
223
- search_button.click(fn=find_nearest_store, inputs=[address, lat, lon], outputs=output_table)
224
-
225
- gps_button.click(
226
- None,
227
- None,
228
- [lat, lon],
229
- js="""
230
- () => {
231
- return new Promise((resolve) => {
232
- if (!navigator.geolocation) {
233
- alert("您的瀏覽器不支援地理位置功能");
234
- resolve([0, 0]);
235
- return;
236
- }
237
- navigator.geolocation.getCurrentPosition(
238
- (position) => {
239
- resolve([position.coords.latitude, position.coords.longitude]);
240
- },
241
- (error) => {
242
- alert("無法取得位置:" + error.message);
 
 
 
 
 
 
243
  resolve([0, 0]);
 
244
  }
245
- );
246
- });
247
- }
248
- """
249
- )
250
-
251
- def main():
252
- demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
 
 
 
 
 
 
 
253
 
254
  if __name__ == "__main__":
255
  main()
 
7
 
8
  # =============== 7-11 所需常數 ===============
9
  # 請確認此處的 MID_V 是否有效,若過期請更新
10
+ MID_V = "W0_DiF4DlgU5OeQoRswrRcaaNHMWOL7K3ra3385ocZcv-bBOWySZvoUtH6j-7pjiccl0C5h30uRUNbJXsABCKMqiekSb7tdiBNdVq8Ro5jgk6sgvhZla5iV0H3-8dZfASc7AhEm85679LIK3hxN7Sam6D0LAnYK9Lb0DZhn7xeTeksB4IsBx4Msr_VI"
11
  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"
12
  API_7_11_BASE = "https://lovefood.openpoint.com.tw/LoveFood/api"
13
 
 
15
  FAMILY_PROJECT_CODE = "202106302" # 若有需要請自行調整
16
  API_FAMILY = "https://stamp.family.com.tw/api/maps/MapProductInfo"
17
 
 
 
18
 
 
 
 
19
  def get_7_11_token():
20
  url = f"{API_7_11_BASE}/Auth/FrontendAuth/AccessToken?mid_v={MID_V}"
21
+ headers = {"user-agent": USER_AGENT_7_11}
 
 
22
  resp = requests.post(url, headers=headers, data="")
23
  resp.raise_for_status()
24
  js = resp.json()
25
  if not js.get("isSuccess"):
26
  raise RuntimeError(f"取得 7-11 token 失敗: {js}")
27
+ return js["element"]
 
28
 
 
 
 
29
  def get_7_11_nearby_stores(token, lat, lon):
30
  url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetNearbyStoreList?token={token}"
31
  headers = {
 
43
  raise RuntimeError(f"取得 7-11 附近門市失敗: {js}")
44
  return js["element"].get("StoreStockItemList", [])
45
 
 
 
 
46
  def get_7_11_store_detail(token, lat, lon, store_no):
47
  url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetStoreDetail?token={token}"
48
  headers = {
 
60
  raise RuntimeError(f"取得 7-11 門市({store_no})資料失敗: {js}")
61
  return js["element"].get("StoreStockItem", {})
62
 
 
 
 
63
  def get_family_nearby_stores(lat, lon):
64
+ headers = {"Content-Type": "application/json;charset=utf-8"}
 
 
65
  body = {
66
  "ProjectCode": FAMILY_PROJECT_CODE,
67
  "latitude": lat,
 
70
  resp = requests.post(API_FAMILY, headers=headers, json=body)
71
  resp.raise_for_status()
72
  js = resp.json()
 
73
  if js.get("code") != 1:
74
  raise RuntimeError(f"取得全家門市資料失敗: {js}")
75
  return js["data"]
76
 
77
+ def find_nearest_store(address, lat, lon, distance_km):
78
+ """
79
+ distance_km: 從下拉選單取得的「公里」(字串),例如 '3' or '5' ...
80
+ """
81
+ print(f"🔍 收到查詢請求: address={address}, lat={lat}, lon={lon}, distance_km={distance_km}")
82
  if lat == 0 or lon == 0:
83
  return [["❌ 請輸入地址或提供 GPS 座標", "", "", "", ""]]
84
 
85
+ # 將 km 轉成公尺
86
+ max_distance = float(distance_km) * 1000
87
+
88
  result_rows = []
89
 
90
  # ------------------ 7-11 ------------------
 
93
  nearby_stores_711 = get_7_11_nearby_stores(token_711, lat, lon)
94
  for store in nearby_stores_711:
95
  dist_m = store.get("Distance", 999999)
96
+ if dist_m <= max_distance:
97
  store_no = store.get("StoreNo")
98
  store_name = store.get("StoreName", "7-11 未提供店名")
99
  remaining_qty = store.get("RemainingQty", 0)
 
104
  for item in cat.get("ItemList", []):
105
  item_name = item.get("ItemName", "")
106
  item_qty = item.get("RemainingQty", 0)
 
107
  row = [
108
  f"7-11 {store_name}",
109
  f"{dist_m:.1f} m",
110
  f"{cat_name} - {item_name}",
111
  str(item_qty),
112
+ dist_m # 用來排序
113
  ]
114
  result_rows.append(row)
115
  else:
 
118
  f"{dist_m:.1f} m",
119
  "即期品 0 項",
120
  "0",
121
+ dist_m
122
  ]
123
  result_rows.append(row)
124
  except Exception as e:
 
129
  nearby_stores_family = get_family_nearby_stores(lat, lon)
130
  for store in nearby_stores_family:
131
  dist_m = store.get("distance", 999999)
132
+ if dist_m <= max_distance:
133
  store_name = store.get("name", "全家 未提供店名")
134
  info_list = store.get("info", [])
135
  has_item = False
 
147
  f"{dist_m:.1f} m",
148
  f"{big_cat_name} - {subcat_name} - {product_name}",
149
  str(qty),
150
+ dist_m
151
  ]
152
  result_rows.append(row)
153
  if not has_item:
 
156
  f"{dist_m:.1f} m",
157
  "即期品 0 項",
158
  "0",
159
+ dist_m
160
  ]
161
  result_rows.append(row)
162
  except Exception as e:
163
  print(f"❌ 取得全家 即期品時發生錯誤: {e}")
164
 
165
  if not result_rows:
166
+ return [["❌ 附近沒有即期食品 (在所選公里範圍內)", "", "", "", ""]]
167
 
168
+ # 排序:依照最後一欄 (float 距離) 做由小到大排序
 
 
169
  result_rows.sort(key=lambda x: x[4])
170
+ # 移除最後一欄 (不顯示給前端)
 
171
  for row in result_rows:
172
+ row.pop()
173
 
174
  return result_rows
175
 
176
+ # ========== Gradio 介面 ==========
177
+
 
178
  import gradio as gr
179
 
180
+ def main():
181
+ with gr.Blocks() as demo:
182
+ gr.Markdown("## 台灣7-11 和 family全家���利商店「即期食品」 乞丐時光搜尋")
183
+ gr.Markdown("""
184
+ 1. 按下「使用目前位置」或自行輸入緯度/經度
185
+ 2. 選擇「搜尋範圍 (公里)
186
+ 3. 點選「搜尋」查詢 7-11 / 全家family 的即期品
187
+ 4. 意見反應 telegram @a7a8a9abc
188
+ """)
189
+
190
+ address = gr.Textbox(label="輸入地址(可留空)")
191
+ lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
192
+ lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
193
+
194
+ # 下拉選單,提供可選距離 (公里)
195
+ distance_dropdown = gr.Dropdown(
196
+ label="搜尋範圍 (公里)",
197
+ choices=["3", "5", "7", "13", "21"],
198
+ value="3", # 預設 3 公里
199
+ interactive=True
200
+ )
201
+
202
+ with gr.Row():
203
+ gps_button = gr.Button("📍 ❶ 使用目前位置-先按這個 並等待3秒 ", elem_id="gps-btn")
204
+ search_button = gr.Button("🔍 ❷ 搜尋 ")
205
+
206
+ output_table = gr.Dataframe(
207
+ headers=["門市", "距離 (m)", "商品/即期食品", "數量"],
208
+ interactive=False
209
+ )
210
+
211
+ # 將 distance_dropdown 傳入函式
212
+ search_button.click(
213
+ fn=find_nearest_store,
214
+ inputs=[address, lat, lon, distance_dropdown],
215
+ outputs=output_table
216
+ )
217
+
218
+ gps_button.click(
219
+ None,
220
+ None,
221
+ [lat, lon],
222
+ js="""
223
+ () => {
224
+ return new Promise((resolve) => {
225
+ if (!navigator.geolocation) {
226
+ alert("您的瀏覽器不支援地理位置功能");
227
  resolve([0, 0]);
228
+ return;
229
  }
230
+ navigator.geolocation.getCurrentPosition(
231
+ (position) => {
232
+ resolve([position.coords.latitude, position.coords.longitude]);
233
+ },
234
+ (error) => {
235
+ alert("無法取得位置:" + error.message);
236
+ resolve([0, 0]);
237
+ }
238
+ );
239
+ });
240
+ }
241
+ """
242
+ )
243
+
244
+ demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
245
 
246
  if __name__ == "__main__":
247
  main()
app3.py ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ import os
5
+ import pandas as pd
6
+ from geopy.distance import geodesic
7
+
8
+ # =============== 7-11 所需常數 ===============
9
+ # 請確認此處的 MID_V 是否有效,若過期請更新
10
+ MID_V = "W0_DiF4DlgU5OeQoRswrRcaaNHMWOL7K3ra3385ocZcv-bBOWySZvoUtH6j-7pjiccl0C5h30uRUNbJXsABCKMqiekSb7tdiBNdVq8Ro5jgk6sgvhZla5iV0H3-8dZfASc7AhEm85679LIK3hxN7Sam6D0LAnYK9Lb0DZhn7xeTeksB4IsBx4Msr_VI" # 請填入有效的 mid_v
11
+ 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"
12
+ API_7_11_BASE = "https://lovefood.openpoint.com.tw/LoveFood/api"
13
+
14
+ # =============== FamilyMart 所需常數 ===============
15
+ FAMILY_PROJECT_CODE = "202106302" # 若有需要請自行調整
16
+ API_FAMILY = "https://stamp.family.com.tw/api/maps/MapProductInfo"
17
+
18
+ # 3 公里範圍
19
+ MAX_DISTANCE = 3000
20
+
21
+ # -----------------------------------------------------------
22
+ # 7-11: 取得 AccessToken
23
+ # -----------------------------------------------------------
24
+ def get_7_11_token():
25
+ url = f"{API_7_11_BASE}/Auth/FrontendAuth/AccessToken?mid_v={MID_V}"
26
+ headers = {
27
+ "user-agent": USER_AGENT_7_11
28
+ }
29
+ resp = requests.post(url, headers=headers, data="")
30
+ resp.raise_for_status()
31
+ js = resp.json()
32
+ if not js.get("isSuccess"):
33
+ raise RuntimeError(f"取得 7-11 token 失敗: {js}")
34
+ token = js["element"]
35
+ return token
36
+
37
+ # -----------------------------------------------------------
38
+ # 7-11: 取得附近門市清單 (含剩餘即期品總數量)
39
+ # -----------------------------------------------------------
40
+ def get_7_11_nearby_stores(token, lat, lon):
41
+ url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetNearbyStoreList?token={token}"
42
+ headers = {
43
+ "user-agent": USER_AGENT_7_11,
44
+ "content-type": "application/json",
45
+ }
46
+ body = {
47
+ "CurrentLocation": {"Latitude": lat, "Longitude": lon},
48
+ "SearchLocation": {"Latitude": lat, "Longitude": lon}
49
+ }
50
+ resp = requests.post(url, headers=headers, json=body)
51
+ resp.raise_for_status()
52
+ js = resp.json()
53
+ if not js.get("isSuccess"):
54
+ raise RuntimeError(f"取得 7-11 附近門市失敗: {js}")
55
+ return js["element"].get("StoreStockItemList", [])
56
+
57
+ # -----------------------------------------------------------
58
+ # 7-11: 取得單一門市的即期品清單
59
+ # -----------------------------------------------------------
60
+ def get_7_11_store_detail(token, lat, lon, store_no):
61
+ url = f"{API_7_11_BASE}/Search/FrontendStoreItemStock/GetStoreDetail?token={token}"
62
+ headers = {
63
+ "user-agent": USER_AGENT_7_11,
64
+ "content-type": "application/json",
65
+ }
66
+ body = {
67
+ "CurrentLocation": {"Latitude": lat, "Longitude": lon},
68
+ "StoreNo": store_no
69
+ }
70
+ resp = requests.post(url, headers=headers, json=body)
71
+ resp.raise_for_status()
72
+ js = resp.json()
73
+ if not js.get("isSuccess"):
74
+ raise RuntimeError(f"取得 7-11 門市({store_no})資料失敗: {js}")
75
+ return js["element"].get("StoreStockItem", {})
76
+
77
+ # -----------------------------------------------------------
78
+ # FamilyMart: 取得附近門市即期品清單 (單次呼叫可拿到所有商品細項)
79
+ # -----------------------------------------------------------
80
+ def get_family_nearby_stores(lat, lon):
81
+ headers = {
82
+ "Content-Type": "application/json;charset=utf-8",
83
+ }
84
+ body = {
85
+ "ProjectCode": FAMILY_PROJECT_CODE,
86
+ "latitude": lat,
87
+ "longitude": lon
88
+ }
89
+ resp = requests.post(API_FAMILY, headers=headers, json=body)
90
+ resp.raise_for_status()
91
+ js = resp.json()
92
+ # 根據回傳範例,成功時 code 為 1
93
+ if js.get("code") != 1:
94
+ raise RuntimeError(f"取得全家門市資料失敗: {js}")
95
+ return js["data"]
96
+
97
+ # -----------------------------------------------------------
98
+ # Gradio 查詢邏輯
99
+ # -----------------------------------------------------------
100
+ def find_nearest_store(address, lat, lon):
101
+ print(f"🔍 收到查詢請求: address={address}, lat={lat}, lon={lon}")
102
+ if lat == 0 or lon == 0:
103
+ return [["❌ 請輸入地址或提供 GPS 座標", "", "", "", ""]]
104
+
105
+ result_rows = []
106
+
107
+ # ------------------ 7-11 ------------------
108
+ try:
109
+ token_711 = get_7_11_token()
110
+ nearby_stores_711 = get_7_11_nearby_stores(token_711, lat, lon)
111
+ for store in nearby_stores_711:
112
+ dist_m = store.get("Distance", 999999)
113
+ if dist_m <= MAX_DISTANCE:
114
+ store_no = store.get("StoreNo")
115
+ store_name = store.get("StoreName", "7-11 未提供店名")
116
+ remaining_qty = store.get("RemainingQty", 0)
117
+ if remaining_qty > 0:
118
+ detail = get_7_11_store_detail(token_711, lat, lon, store_no)
119
+ for cat in detail.get("CategoryStockItems", []):
120
+ cat_name = cat.get("Name", "")
121
+ for item in cat.get("ItemList", []):
122
+ item_name = item.get("ItemName", "")
123
+ item_qty = item.get("RemainingQty", 0)
124
+ # 在最後加一個 float 距離欄位以便排序
125
+ row = [
126
+ f"7-11 {store_name}",
127
+ f"{dist_m:.1f} m",
128
+ f"{cat_name} - {item_name}",
129
+ str(item_qty),
130
+ dist_m # 供排序用
131
+ ]
132
+ result_rows.append(row)
133
+ else:
134
+ row = [
135
+ f"7-11 {store_name}",
136
+ f"{dist_m:.1f} m",
137
+ "即期品 0 項",
138
+ "0",
139
+ dist_m # 供排序用
140
+ ]
141
+ result_rows.append(row)
142
+ except Exception as e:
143
+ print(f"❌ 取得 7-11 即期品時發生錯誤: {e}")
144
+
145
+ # ------------------ FamilyMart ------------------
146
+ try:
147
+ nearby_stores_family = get_family_nearby_stores(lat, lon)
148
+ for store in nearby_stores_family:
149
+ dist_m = store.get("distance", 999999)
150
+ if dist_m <= MAX_DISTANCE:
151
+ store_name = store.get("name", "全家 未提供店名")
152
+ info_list = store.get("info", [])
153
+ has_item = False
154
+ for big_cat in info_list:
155
+ big_cat_name = big_cat.get("name", "")
156
+ for subcat in big_cat.get("categories", []):
157
+ subcat_name = subcat.get("name", "")
158
+ for product in subcat.get("products", []):
159
+ product_name = product.get("name", "")
160
+ qty = product.get("qty", 0)
161
+ if qty > 0:
162
+ has_item = True
163
+ row = [
164
+ f"全家 {store_name}",
165
+ f"{dist_m:.1f} m",
166
+ f"{big_cat_name} - {subcat_name} - {product_name}",
167
+ str(qty),
168
+ dist_m # 供排序用
169
+ ]
170
+ result_rows.append(row)
171
+ if not has_item:
172
+ row = [
173
+ f"全家 {store_name}",
174
+ f"{dist_m:.1f} m",
175
+ "即期品 0 項",
176
+ "0",
177
+ dist_m # 供排序用
178
+ ]
179
+ result_rows.append(row)
180
+ except Exception as e:
181
+ print(f"❌ 取得全家 即期品時發生錯誤: {e}")
182
+
183
+ if not result_rows:
184
+ return [["❌ 附近 3 公里內沒有即期食品", "", "", "", ""]]
185
+
186
+ # ============= 在這裡進行排序 =============
187
+ # result_rows 的結構是 [門市, 距離(字串), 商品, 數量, float_distance]
188
+ # 我們要依照最後一欄 float_distance 做由小到大排序
189
+ result_rows.sort(key=lambda x: x[4])
190
+
191
+ # 排序完之後,再把最後一欄刪掉 (不顯示給使用者)
192
+ for row in result_rows:
193
+ row.pop() # 移除 index=4 (float_distance)
194
+
195
+ return result_rows
196
+
197
+ # -----------------------------------------------------------
198
+ # Gradio 介面
199
+ # -----------------------------------------------------------
200
+ import gradio as gr
201
+
202
+ with gr.Blocks() as demo:
203
+ gr.Markdown("## 便利商店「即期食品」搜尋示範")
204
+ gr.Markdown("""
205
+ 1. 按下「使用目前位置」或自行輸入緯度/經度
206
+ 2. 點選「搜尋」查詢 3 公里內 7-11 / 全家的即期品
207
+ 3. 若要執行,需要有效的 mid_v (7-11 愛食記憶官網)
208
+ 4. 在 Logs 查看詳細錯誤或除錯資訊
209
+ """)
210
+ address = gr.Textbox(label="輸入地址(可留空)")
211
+ lat = gr.Number(label="GPS 緯度", value=0, elem_id="lat")
212
+ lon = gr.Number(label="GPS 經度", value=0, elem_id="lon")
213
+
214
+ with gr.Row():
215
+ gps_button = gr.Button("📍 ❶ 使用目前位置-先按這個 並等待3秒 ", elem_id="gps-btn")
216
+ search_button = gr.Button("🔍 ❷ 搜尋 ")
217
+
218
+ output_table = gr.Dataframe(
219
+ headers=["門市", "距離 (m)", "商品/即期食品", "數量"],
220
+ interactive=False
221
+ )
222
+
223
+ search_button.click(fn=find_nearest_store, inputs=[address, lat, lon], outputs=output_table)
224
+
225
+ gps_button.click(
226
+ None,
227
+ None,
228
+ [lat, lon],
229
+ js="""
230
+ () => {
231
+ return new Promise((resolve) => {
232
+ if (!navigator.geolocation) {
233
+ alert("您的瀏覽器不支援地理位置功能");
234
+ resolve([0, 0]);
235
+ return;
236
+ }
237
+ navigator.geolocation.getCurrentPosition(
238
+ (position) => {
239
+ resolve([position.coords.latitude, position.coords.longitude]);
240
+ },
241
+ (error) => {
242
+ alert("無法取得位置:" + error.message);
243
+ resolve([0, 0]);
244
+ }
245
+ );
246
+ });
247
+ }
248
+ """
249
+ )
250
+
251
+ def main():
252
+ demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
253
+
254
+ if __name__ == "__main__":
255
+ main()