diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/dealverse/.mvn/wrapper/maven-wrapper.properties b/dealverse/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/dealverse/.mvn/wrapper/maven-wrapper.properties @@ -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 diff --git a/dealverse/mvnw b/dealverse/mvnw old mode 100644 new mode 100755 diff --git a/dealverse/mvnw.cmd b/dealverse/mvnw.cmd index 92450f9..5761d94 100644 --- a/dealverse/mvnw.cmd +++ b/dealverse/mvnw.cmd @@ -1,189 +1,189 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/dealverse/src/main/java/com/example/dealverse/GoogleSearchService.java b/dealverse/src/main/java/com/example/dealverse/GoogleSearchService.java index 84cecc6..b9e5d73 100644 --- a/dealverse/src/main/java/com/example/dealverse/GoogleSearchService.java +++ b/dealverse/src/main/java/com/example/dealverse/GoogleSearchService.java @@ -29,8 +29,10 @@ public class GoogleSearchService { public Map search(String query) { var results = new LinkedHashMap(); - 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); @@ -39,16 +41,17 @@ public Map search(String query) { ResponseEntity resp = restTemplate.getForEntity(url, Map.class); Map 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); } diff --git a/dealverse/src/main/java/com/example/dealverse/MomoService.java b/dealverse/src/main/java/com/example/dealverse/MomoService.java new file mode 100644 index 0000000..5cb566a --- /dev/null +++ b/dealverse/src/main/java/com/example/dealverse/MomoService.java @@ -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 map = new LinkedMultiValueMap<>(); + map.add("flag", "2018"); // 2018 通常是用於查詢價格/庫存的 flag + map.add("goodsCode", goodsCode); + + HttpEntity> request = new HttpEntity<>(map, headers); + + try { + // 3. 發送 POST 請求 + ResponseEntity 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"; + } +} diff --git a/dealverse/src/main/java/com/example/dealverse/SearchController.java b/dealverse/src/main/java/com/example/dealverse/SearchController.java index 7aaf815..88f1f49 100644 --- a/dealverse/src/main/java/com/example/dealverse/SearchController.java +++ b/dealverse/src/main/java/com/example/dealverse/SearchController.java @@ -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 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()) { diff --git a/dealverse/target/classes/com/example/dealverse/GoogleSearchService.class b/dealverse/target/classes/com/example/dealverse/GoogleSearchService.class index fb21291..9b488cd 100644 Binary files a/dealverse/target/classes/com/example/dealverse/GoogleSearchService.class and b/dealverse/target/classes/com/example/dealverse/GoogleSearchService.class differ diff --git a/dealverse/target/classes/com/example/dealverse/MomoService.class b/dealverse/target/classes/com/example/dealverse/MomoService.class new file mode 100644 index 0000000..6459443 Binary files /dev/null and b/dealverse/target/classes/com/example/dealverse/MomoService.class differ diff --git a/dealverse/target/classes/com/example/dealverse/SearchController.class b/dealverse/target/classes/com/example/dealverse/SearchController.class index 3931341..ec7d7bd 100644 Binary files a/dealverse/target/classes/com/example/dealverse/SearchController.class and b/dealverse/target/classes/com/example/dealverse/SearchController.class differ diff --git a/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst index 91e02bc..e69de29 100644 --- a/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +++ b/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -1 +0,0 @@ -com\example\dealverse\DealverseApplication.class diff --git a/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst index a76e6be..2c10359 100644 --- a/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ b/dealverse/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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 diff --git a/finalproject/dealverse_pchome b/finalproject/dealverse_pchome deleted file mode 160000 index 70df59a..0000000 --- a/finalproject/dealverse_pchome +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 70df59a02fafb74ee14bba39cd63810e7af62e12 diff --git a/finalproject/mvnw b/finalproject/mvnw old mode 100644 new mode 100755 diff --git a/finalproject/src/main/java/com/example/dealverse/service/JsoupPriceExtractor.java b/finalproject/src/main/java/com/example/dealverse/service/JsoupPriceExtractor.java index 9de60c1..9f07d73 100644 --- a/finalproject/src/main/java/com/example/dealverse/service/JsoupPriceExtractor.java +++ b/finalproject/src/main/java/com/example/dealverse/service/JsoupPriceExtractor.java @@ -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) { diff --git a/finalproject/src/main/java/com/example/dealverse/service/OpenAiService.java b/finalproject/src/main/java/com/example/dealverse/service/OpenAiService.java new file mode 100644 index 0000000..788106c --- /dev/null +++ b/finalproject/src/main/java/com/example/dealverse/service/OpenAiService.java @@ -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 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 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 ""; // 發生例外時回傳空字串 + } + } +} diff --git a/finalproject/src/main/java/com/example/dealverse/web/DealVerseSearchService.java b/finalproject/src/main/java/com/example/dealverse/web/DealVerseSearchService.java index c898666..9747002 100644 --- a/finalproject/src/main/java/com/example/dealverse/web/DealVerseSearchService.java +++ b/finalproject/src/main/java/com/example/dealverse/web/DealVerseSearchService.java @@ -19,16 +19,56 @@ public DealVerseSearchService(SearchService searchService) { this.searchService = searchService; } + /** + * 舊的搜尋方法 (保留相容性) + * + * @param keyword 搜尋關鍵字 + * @return 標題與 URL 的對應表 + */ public Map searchTitleUrl(String keyword) { + return searchTitleUrl(keyword, null); + } + + /** + * 新的搜尋方法 (支援平台過濾) + * + * @param keyword 搜尋關鍵字 + * @param sources 允許的平台列表 (例如 ["Shopee", "Momo"]),null 或空表示不過濾 + * @return 標題與 URL 的對應表 + */ + public Map searchTitleUrl(String keyword, List sources) { Query q = new Query(keyword); + + // 取得搜尋引擎回傳的 Top 10 結果 List results = searchService.search(q, 10); Map 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; diff --git a/finalproject/src/main/java/com/example/dealverse/web/SearchController.java b/finalproject/src/main/java/com/example/dealverse/web/SearchController.java index 042241e..4352791 100644 --- a/finalproject/src/main/java/com/example/dealverse/web/SearchController.java +++ b/finalproject/src/main/java/com/example/dealverse/web/SearchController.java @@ -1,37 +1,69 @@ package com.example.dealverse.web; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Map; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; +import com.example.dealverse.service.OpenAiService; @Controller public class SearchController { private final DealVerseSearchService dealVerseSearchService; + private final OpenAiService openAiService; // 1. 宣告變數 - public SearchController(DealVerseSearchService dealVerseSearchService) { + // 2. 建構子注入 OpenAiService + public SearchController(DealVerseSearchService dealVerseSearchService, OpenAiService openAiService) { this.dealVerseSearchService = dealVerseSearchService; + this.openAiService = openAiService; } @GetMapping("/") - public String index() { + public String index(Model model) { return "index"; } @GetMapping("/search") - public String search(@RequestParam("keyword") String keyword, Model model) { + public String search( + @RequestParam("keyword") String keyword, + @RequestParam(value = "sources", required = false) List sources, // 1. 接收平台列表 + Model model) { + try { - Map results = dealVerseSearchService.searchTitleUrl(keyword); + // 2. 處理預設值:如果沒選,預設找這三家 + if (sources == null || sources.isEmpty()) { + sources = Arrays.asList("Shopee", "Momo", "PChome"); + } + + // 3. AI 翻譯/優化查詢 (從中文轉精準英文/關鍵字) + String optimizedKeyword = openAiService.refineQuery(keyword); + + // 4. AI 取得推薦商品 (互動邏輯:搜尋後跳出推薦) + String recString = openAiService.getRecommendations(keyword); + List recommendations = new ArrayList<>(); + if (recString != null && !recString.isEmpty()) { + recommendations = Arrays.asList(recString.split(",")); + } + + // 5. 執行搜尋 (傳入 sources 進行過濾) + Map results = dealVerseSearchService.searchTitleUrl(optimizedKeyword, sources); + + // 6. 回傳資料給 View model.addAttribute("keyword", keyword); + model.addAttribute("optimizedKeyword", optimizedKeyword); + model.addAttribute("recommendations", recommendations); model.addAttribute("results", results); - model.addAttribute("error", null); + model.addAttribute("sources", sources); // 讓前端 checkbox 保持勾選狀態 + } catch (Exception e) { + e.printStackTrace(); model.addAttribute("keyword", keyword); - model.addAttribute("results", null); - model.addAttribute("error", e.getMessage()); + model.addAttribute("error", "搜尋發生錯誤: " + e.getMessage()); } return "index"; } diff --git a/finalproject/src/main/resources/application.properties b/finalproject/src/main/resources/application.properties index e1a6200..2ff671c 100644 --- a/finalproject/src/main/resources/application.properties +++ b/finalproject/src/main/resources/application.properties @@ -1 +1,5 @@ spring.application.name=finalproject + +# 新增這一行 (請換成你真正的 Key) +openai.api.key=YOUR_OPENAI_API_KEY_HERE +openai.model=gpt-4o-mini diff --git a/finalproject/src/main/resources/templates/index.html b/finalproject/src/main/resources/templates/index.html index a4ede92..4e83874 100644 --- a/finalproject/src/main/resources/templates/index.html +++ b/finalproject/src/main/resources/templates/index.html @@ -3,120 +3,518 @@ - DealVerse Search + + DealVerse - AI-Powered Price Comparison + + + + + + + +
+
+
+
+
+
+
+ +
- + +
+ +
AI-Powered Multi-Platform Price Comparison
+
+ + +
+
+ - - - -
+ +
+ 選擇平台 +
+
+ + +
+
+ + +
+
+ + +
+
+
-
- -
- 沒有找到相關商品,請試試別的關鍵字或檢查後端設定。 + +
+ 🤖 AI 推薦相關商品 +
+ +
+
+ +
+ + +
+
搜尋結果
+ + +
+ AI 優化查詢:
- + +
+ 沒有找到相關商品,請試試其他關鍵字或選擇不同平台。 +
+ +
- +
- +
+
+ + input.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + form.submit(); + } + }); + \ No newline at end of file