Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}
3 changes: 3 additions & 0 deletions dealverse/.mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
Empty file modified dealverse/mvnw
100644 → 100755
Empty file.
378 changes: 189 additions & 189 deletions dealverse/mvnw.cmd

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ public class GoogleSearchService {
public Map<String, String> search(String query) {
var results = new LinkedHashMap<String, String>();

if (!enabled) return results;
if (apiKey == null || apiKey.isBlank() || cx == null || cx.isBlank()) return results;
if (!enabled)
return results;
if (apiKey == null || apiKey.isBlank() || cx == null || cx.isBlank())
return results;

try {
String q = URLEncoder.encode(query, StandardCharsets.UTF_8);
Expand All @@ -39,16 +41,17 @@ public Map<String, String> search(String query) {

ResponseEntity<Map> resp = restTemplate.getForEntity(url, Map.class);
Map<String, Object> body = resp.getBody();
if (body == null) return results;
if (body == null)
return results;

Object itemsObj = body.get("items");
if (itemsObj instanceof List<?> items) {
for (Object it : items) {
if (it instanceof Map<?, ?> item) {
Object titleObj = item.get("title");
Object linkObj = item.get("link");
Object linkObj = item.get("link");
String title = titleObj == null ? "" : titleObj.toString();
String link = linkObj == null ? "" : linkObj.toString();
String link = linkObj == null ? "" : linkObj.toString();
if (!title.isBlank() && !link.isBlank()) {
results.put(title, link);
}
Expand Down
66 changes: 66 additions & 0 deletions dealverse/src/main/java/com/example/dealverse/MomoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.dealverse;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

@Service
public class MomoService {

private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;

public MomoService(RestTemplate restTemplate, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}

public String getMomoPrice(String goodsCode) {
String url = "https://www.momoshop.com.tw/ajax/ajaxTool.jsp";

// 1. 設定 Headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headers.set("Referer", "https://www.momoshop.com.tw/goods/GoodsDetail.jsp?i_code=" + goodsCode); // 必須有 Referer

// 2. 設定 Payload (Form Data)
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("flag", "2018"); // 2018 通常是用於查詢價格/庫存的 flag
map.add("goodsCode", goodsCode);

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

try {
// 3. 發送 POST 請求
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);

if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
// 4. 解析 JSON
JsonNode root = objectMapper.readTree(response.getBody());

// 5. 提取價格
if (root.has("price")) {
return root.get("price").asText();
} else if (root.has("marketPrice")) {
return root.get("marketPrice").asText();
} else {
// Log response for debugging if needed
System.out.println("MOMO Response: " + response.getBody());
}
}
} catch (Exception e) {
e.printStackTrace();
}

return "N/A";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,31 @@
@Controller
public class SearchController {
private final GoogleSearchService google;
private final MomoService momoService;

public SearchController(GoogleSearchService google) {
public SearchController(GoogleSearchService google, MomoService momoService) {
this.google = google;
this.momoService = momoService;
}

@GetMapping("/")
public String home() { return "index"; }
public String home() {
return "index";
}

@GetMapping("/ping")
@ResponseBody
public String ping() { return "ok"; }
public String ping() {
return "ok";
}

@GetMapping("/search")
public String search(@RequestParam String keyword, Model model) {
try {
Map<String, String> results = google.search(keyword);
// 範例:使用 iPhone 15 的商品編號測試
String momoPrice = momoService.getMomoPrice("14364332");
model.addAttribute("momoPrice", momoPrice);
model.addAttribute("keyword", keyword);
model.addAttribute("results", results);
if (results.isEmpty()) {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
com\example\dealverse\DealverseApplication.class
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
C:\data structure\2025Fall-DS\HW8\dealverse\src\main\java\com\example\dealverse\DealverseApplication.java
/Users/frank/Documents/student/2025Fall-DS/Datastructure_G2/dealverse/src/main/java/com/example/dealverse/DealverseApplication.java
/Users/frank/Documents/student/2025Fall-DS/Datastructure_G2/dealverse/src/main/java/com/example/dealverse/GoogleSearchService.java
/Users/frank/Documents/student/2025Fall-DS/Datastructure_G2/dealverse/src/main/java/com/example/dealverse/SearchController.java
1 change: 0 additions & 1 deletion finalproject/dealverse_pchome
Submodule dealverse_pchome deleted from 70df59
Empty file modified finalproject/mvnw
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
@Primary
public class JsoupPriceExtractor implements PriceExtractor {

private static final Pattern PRICE_PATTERN =
Pattern.compile("(\\d{1,3}(,\\d{3})*|\\d{3,})(?=\\s*(元|NT|$|\\$))");
private static final Pattern PRICE_PATTERN = Pattern.compile("(\\d{1,3}(,\\d{3})*|\\d{3,})(?=\\s*(元|NT|$|\\$))");

@Override
public Integer extractPrice(String url, String source) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.example.dealverse.service;

import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

@Service
public class OpenAiService {

@Value("${openai.api.key}")
private String apiKey;

@Value("${openai.model}")
private String model;

private final HttpClient httpClient;

public OpenAiService() {
// 設定 3 秒超時,避免搜尋卡太久
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
}

public String refineQuery(String userQuery) {
// 如果使用者輸入太短或已經是英文,可能不需要翻譯 (可選邏輯)
if (userQuery == null || userQuery.trim().isEmpty()) {
return "";
}

try {
// 1. 準備 Prompt:這段是關鍵,告訴 AI 把口語轉成購物關鍵字
String systemPrompt = "You are a query optimizer for a shopping bot. " +
"Your task: Convert user input into concise English keywords for Google Shopping search. " +
"CRITICAL RULES: " +
"1. ALWAYS translate non-English text (Chinese, Japanese, etc.) to English. " +
"2. Use SINGLE English words or hyphenated terms (NO SPACES between words). " +
"3. Remove filler words and keep only essential keywords. " +
"4. Output ONLY the English keyword, nothing else. " +
"Examples: " +
"- Input: '手機' → Output: 'phone' " +
"- Input: '記憶體' → Output: 'memory' " +
"- Input: '筆記型電腦' → Output: 'laptop' " +
"- Input: '藍牙耳機' → Output: 'bluetooth-headphones' " +
"- Input: '無線滑鼠' → Output: 'wireless-mouse'";

// 2. 建構 JSON Body
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", model);

JSONArray messages = new JSONArray();
messages.put(new JSONObject().put("role", "system").put("content", systemPrompt));
messages.put(new JSONObject().put("role", "user").put("content", userQuery));
jsonBody.put("messages", messages);
jsonBody.put("temperature", 0.3); // 低溫度讓結果更穩定

// 3. 發送 HTTP POST 請求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.openai.com/v1/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody.toString()))
.timeout(Duration.ofSeconds(5)) // 等待回應最多 5 秒
.build();

HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

// 4. 解析回傳結果
if (response.statusCode() == 200) {
JSONObject responseJson = new JSONObject(response.body());
String refinedContent = responseJson.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content");

// 去除可能的多餘引號或換行
return refinedContent.trim().replaceAll("^\"|\"$", "");
} else {
System.err.println("OpenAI API Error: " + response.statusCode() + " " + response.body());
return userQuery; // 失敗時降級使用原始查詢
}

} catch (Exception e) {
e.printStackTrace();
return userQuery; // 發生例外時降級使用原始查詢
}
}

/**
* 取得 AI 推薦的相關商品關鍵字
*
* @param userQuery 使用者輸入的查詢
* @return 逗號分隔的推薦關鍵字字串,例如 "wireless-earbuds,bluetooth-speaker,smartwatch"
*/
public String getRecommendations(String userQuery) {
if (userQuery == null || userQuery.trim().isEmpty()) {
return "";
}

try {
// 1. 準備 Prompt:讓 AI 推薦相關商品
String systemPrompt = "You are a shopping recommendation assistant. " +
"Given a product search query, suggest 3-5 related product keywords that users might also be interested in. "
+
"CRITICAL RULES: " +
"1. Output ONLY comma-separated English keywords (NO spaces after commas). " +
"2. Use single words or hyphenated terms (e.g., 'wireless-mouse', 'laptop-stand'). " +
"3. Suggest products that are complementary or in the same category. " +
"4. NO explanations, NO numbering, ONLY the comma-separated list. " +
"Examples: " +
"- Input: 'phone' → Output: 'phone-case,screen-protector,wireless-charger,earbuds' " +
"- Input: 'laptop' → Output: 'laptop-bag,mouse,keyboard,monitor,laptop-stand' " +
"- Input: 'camera' → Output: 'camera-lens,tripod,memory-card,camera-bag'";

// 2. 建構 JSON Body
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", model);

JSONArray messages = new JSONArray();
messages.put(new JSONObject().put("role", "system").put("content", systemPrompt));
messages.put(new JSONObject().put("role", "user").put("content", userQuery));
jsonBody.put("messages", messages);
jsonBody.put("temperature", 0.7); // 較高溫度讓推薦更多樣化

// 3. 發送 HTTP POST 請求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.openai.com/v1/chat/completions"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody.toString()))
.timeout(Duration.ofSeconds(5))
.build();

HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

// 4. 解析回傳結果
if (response.statusCode() == 200) {
JSONObject responseJson = new JSONObject(response.body());
String recommendations = responseJson.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content");

// 清理結果:移除多餘空白和引號
return recommendations.trim().replaceAll("^\"|\"$", "");
} else {
System.err.println(
"OpenAI API Error (Recommendations): " + response.statusCode() + " " + response.body());
return ""; // 失敗時回傳空字串
}

} catch (Exception e) {
e.printStackTrace();
return ""; // 發生例外時回傳空字串
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,56 @@ public DealVerseSearchService(SearchService searchService) {
this.searchService = searchService;
}

/**
* 舊的搜尋方法 (保留相容性)
*
* @param keyword 搜尋關鍵字
* @return 標題與 URL 的對應表
*/
public Map<String, String> searchTitleUrl(String keyword) {
return searchTitleUrl(keyword, null);
}

/**
* 新的搜尋方法 (支援平台過濾)
*
* @param keyword 搜尋關鍵字
* @param sources 允許的平台列表 (例如 ["Shopee", "Momo"]),null 或空表示不過濾
* @return 標題與 URL 的對應表
*/
public Map<String, String> searchTitleUrl(String keyword, List<String> sources) {
Query q = new Query(keyword);

// 取得搜尋引擎回傳的 Top 10 結果
List<Result> results = searchService.search(q, 10);

Map<String, String> map = new LinkedHashMap<>();
for (Result r : results) {
String title = r.getOffer().getSource() + " - " + r.getOffer().getBrand() + " " + r.getOffer().getModel();
String sourceName = r.getOffer().getSource(); // 例如 "Shopee", "Momo"

// --- 過濾邏輯 ---
// 如果 sources 不為空,且該結果的來源不在 sources 列表中,就跳過
if (sources != null && !sources.isEmpty()) {
boolean isAllowed = false;
for (String allowedSource : sources) {
// 使用 contains 比較寬鬆的匹配 (忽略大小寫)
if (sourceName.toLowerCase().contains(allowedSource.toLowerCase())) {
isAllowed = true;
break;
}
}
if (!isAllowed) {
continue; // 跳過這個結果
}
}
// ----------------

String title = sourceName + " - " + r.getOffer().getBrand() + " " + r.getOffer().getModel();
if (title.trim().isEmpty()) {
title = r.getOffer().getSource();
title = sourceName;
}

// 組合顯示字串 (標題 | 價格)
map.put(title + " | pay=" + r.getPay(), r.getOffer().getUrl());
}
return map;
Expand Down
Loading