Преглед изворни кода

【第一版开发】
1.移除冗余代码
2.ProductHunt Server增加访问密钥及其对应的校验逻辑
3.ProductHunt热榜接口增加榜单日期入参,提高数据查询范围的灵活性
4.ProductHunt热榜接口增加获取数量配置化

ChenYL пре 10 месеци
родитељ
комит
f28dbddb3d
18 измењених фајлова са 181 додато и 440 уклоњено
  1. 32 0
      data-easy/src/main/java/com/dataeasy/server/core/config/ProductHuntProperties.java
  2. 3 1
      data-easy/src/main/java/com/dataeasy/server/feign/ProductHuntFeign.java
  3. 24 3
      data-easy/src/main/java/com/dataeasy/server/task/ProductHuntTask.java
  4. 12 0
      data-easy/src/main/resources/application-dev.yaml
  5. 9 1
      data-easy/src/main/resources/application-prod.yaml
  6. 2 1
      data-easy/src/main/resources/application.yaml
  7. 0 2
      product-hunt/src/main/java/com/producthunt/server/Application.java
  8. 13 0
      product-hunt/src/main/java/com/producthunt/server/core/aop/GlobalExceptionHandler.java
  9. 2 2
      product-hunt/src/main/java/com/producthunt/server/core/config/BizProperties.java
  10. 0 31
      product-hunt/src/main/java/com/producthunt/server/core/config/EncryptorConfig.java
  11. 5 1
      product-hunt/src/main/java/com/producthunt/server/core/config/ProductHuntProperties.java
  12. 27 15
      product-hunt/src/main/java/com/producthunt/server/core/interceptor/AuthInterceptor.java
  13. 17 350
      product-hunt/src/main/java/com/producthunt/server/service/controller/ProductHuntController.java
  14. 3 2
      product-hunt/src/main/java/com/producthunt/server/service/manager/IProductHuntManager.java
  15. 26 27
      product-hunt/src/main/java/com/producthunt/server/service/manager/impl/ProductHuntManagerImpl.java
  16. 2 2
      product-hunt/src/main/resources/application-dev.yaml
  17. 2 2
      product-hunt/src/main/resources/application-prod.yaml
  18. 2 0
      product-hunt/src/main/resources/application.yaml

+ 32 - 0
data-easy/src/main/java/com/dataeasy/server/core/config/ProductHuntProperties.java

@@ -0,0 +1,32 @@
+package com.dataeasy.server.core.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author tyuio
+ * @version 1.0.0
+ * @date 2025/3/14 10:12
+ * @description 自建ProductHunt服务配置类
+ */
+@Data
+@Configuration
+@ConfigurationProperties("product-hunt")
+public class ProductHuntProperties {
+
+    /**
+     * 加密密码
+     */
+    private String password;
+
+    /**
+     * 加密算法
+     */
+    private String algorithm;
+
+    /**
+     * 系统自访问密钥
+     */
+    private String apiKey;
+}

+ 3 - 1
data-easy/src/main/java/com/dataeasy/server/feign/ProductHuntFeign.java

@@ -4,6 +4,7 @@ import com.dataeasy.server.common.pojo.JsonResponse;
 import com.dataeasy.server.feign.dto.PostNode;
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestHeader;
 
 import java.util.List;
 
@@ -18,8 +19,9 @@ public interface ProductHuntFeign {
 
     /**
      * 获取产品热榜TOP30
+     * @param token 访问凭据
      * @return
      */
     @GetMapping("/producthunt/getTop30Posts")
-    JsonResponse<List<PostNode>> getTop30Posts();
+    JsonResponse<List<PostNode>> getTop30Posts(@RequestHeader("Authorization") String token);
 }

+ 24 - 3
data-easy/src/main/java/com/dataeasy/server/task/ProductHuntTask.java

@@ -8,9 +8,12 @@ import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
+import com.dataeasy.server.core.config.ProductHuntProperties;
+import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
+import org.jasypt.iv.RandomIvGenerator;
+import org.jasypt.salt.RandomSaltGenerator;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
@@ -42,7 +45,25 @@ public class ProductHuntTask extends AbstractTask {
     @Autowired
     private IDataProductHuntPostService productHuntPostService;
 
-//    @Scheduled(cron = "0 16 * * * *")
+    /**
+     * 自建ProductHunt服务访问凭据
+     */
+    private String apiToken;
+
+    @Autowired
+    public ProductHuntTask(ProductHuntProperties productHuntProperties) {
+        // 构建加密器
+        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
+        encryptor.setPassword(productHuntProperties.getPassword());
+        encryptor.setAlgorithm(productHuntProperties.getAlgorithm());
+        encryptor.setSaltGenerator(new RandomSaltGenerator());
+        encryptor.setIvGenerator(new RandomIvGenerator());
+
+        // 构建访问凭据
+        apiToken = encryptor.encrypt(productHuntProperties.getApiKey());
+    }
+
+    //    @Scheduled(cron = "0 16 * * * *")
     @Transactional(rollbackFor = Exception.class)
     public void exec() {
         executeMain();
@@ -56,7 +77,7 @@ public class ProductHuntTask extends AbstractTask {
     @Override
     public void fetchData() {
         // 拉取数据
-        JsonResponse<List<PostNode>> jsonResponse = productHuntFeign.getTop30Posts();
+        JsonResponse<List<PostNode>> jsonResponse = productHuntFeign.getTop30Posts(apiToken);
         if (Objects.isNull(jsonResponse)) {
             log.warn("拉取ProductHunt数据失败,返回结果对象为空");
             return;

+ 12 - 0
data-easy/src/main/resources/application-dev.yaml

@@ -46,6 +46,18 @@ hz-api:
   id: ENC(2myOnTLci1DlpQfDkcqN2fGoM24y/fairiO5hH7braM=)
   key: ENC(SK18qyEQdxOACJ2xeNLTGbUZEDi+wRkvDBs4lL/+wCc1ANFwD42P2Q7Ssl3U3uu9fVRQ3BdgV2o=)
 
+# 金融接口
+finance:
+  base-url: http://192.168.123.242:8000
+
+# 自建ProductHunt服务配置
+product-hunt:
+  base-url: http://127.0.0.1:8081
+  # 加密密码
+  password: ENC(2wscdOD7kzByW3VNDQkOZq2BdoM6PS7peINZRwJjEwVubsFN437X3g==)
+  # 系统自访问密钥
+  api-key: ENC(S3TSPw9QuJwvZScgrOv1ORm089BkXy6EJJNeFYwRVxqZnBb176rSXA==)
+
 # 系统配置
 biz:
   tokenPassword: 1234567890

+ 9 - 1
data-easy/src/main/resources/application-prod.yaml

@@ -34,4 +34,12 @@ wx:
 # 接口盒子平台配置
 hz-api:
   id: ENC(xOmDcVBaAxOJHC1bazlmG8m3E082uCTZ4o8jxT7Fi8Q=)
-  key: ENC(I2SZcgbv6EEyEndn4FSMihTlTxVZ3zhxaQAIeJAOdeos3ZH4Nb8QalW1L1phlIK8C/FdPmnsEw8=)
+  key: ENC(I2SZcgbv6EEyEndn4FSMihTlTxVZ3zhxaQAIeJAOdeos3ZH4Nb8QalW1L1phlIK8C/FdPmnsEw8=)
+
+# 自建ProductHunt服务配置
+product-hunt:
+  base-url: http://api-producthunt.zhixinghe1.top
+  # 加密密码
+  password: ENC(V0XvB+A9qK6Xmd1q7M9Bjls8aW4QH3m91GMcr+zpiAIhXFiLALCn+w==)
+  # 系统自访问密钥
+  api-key: ENC(z2rDV5xcbhkO3JuGB83J+0GmLREenc7ucsn0s5POe2NtMtblIgdOfQ==)

+ 2 - 1
data-easy/src/main/resources/application.yaml

@@ -31,4 +31,5 @@ finance:
 
 # 自建ProductHunt服务配置
 product-hunt:
-  base-url: http://127.0.0.1:8080
+  # 加密算法
+  algorithm: PBEWithMD5AndDES

+ 0 - 2
product-hunt/src/main/java/com/producthunt/server/Application.java

@@ -3,7 +3,6 @@ package com.producthunt.server;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.openfeign.EnableFeignClients;
-import org.springframework.scheduling.annotation.EnableScheduling;
 
 /**
  * @author tyuio
@@ -11,7 +10,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
  * @description 程序运行入口
  * @date 2025/2/14 9:57
  */
-@EnableScheduling
 @EnableFeignClients
 @SpringBootApplication
 public class Application {

+ 13 - 0
product-hunt/src/main/java/com/producthunt/server/core/aop/GlobalExceptionHandler.java

@@ -6,6 +6,7 @@ import java.util.stream.Collectors;
 import com.producthunt.server.common.BusinessException;
 import com.producthunt.server.common.JsonResponse;
 import com.producthunt.server.common.LoginException;
+import jakarta.validation.ConstraintViolationException;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.validation.ObjectError;
@@ -57,6 +58,18 @@ public class GlobalExceptionHandler {
         return JsonResponse.fail(String.format("参数错误:%s", errorMsg));
     }
 
+    /**
+     * 方法参数校验异常处理
+     * @param e
+     * @return
+     */
+    @ExceptionHandler(ConstraintViolationException.class)
+    public JsonResponse constraintViolationExceptionHandler(ConstraintViolationException e) {
+        String errorMsg = e.getConstraintViolations().stream().map(error -> error.getMessage())
+                .collect(Collectors.joining(";"));
+        return JsonResponse.fail(String.format("参数错误:%s", errorMsg));
+    }
+
     /**
      * 全局异常处理
      */

+ 2 - 2
product-hunt/src/main/java/com/producthunt/server/core/config/BizConfig.java → product-hunt/src/main/java/com/producthunt/server/core/config/BizProperties.java

@@ -13,12 +13,12 @@ import org.springframework.stereotype.Component;
 @Data
 @Component
 @ConfigurationProperties("biz")
-public class BizConfig {
+public class BizProperties {
 
     /**
      * 加密密钥
      */
-    private String secret;
+    private String password;
 
     /**
      * 加密算法

+ 0 - 31
product-hunt/src/main/java/com/producthunt/server/core/config/EncryptorConfig.java

@@ -1,31 +0,0 @@
-package com.producthunt.server.core.config;
-
-import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
-import org.jasypt.iv.RandomIvGenerator;
-import org.jasypt.salt.RandomSaltGenerator;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-/**
- * @author tyuio
- * @version 1.0.0
- * @description 加解密配置类
- * @date 2025/3/6 17:02
- */
-@Configuration
-public class EncryptorConfig {
-
-    @Autowired
-    private BizConfig bizConfig;
-
-    @Bean
-    public StandardPBEStringEncryptor encryptor() {
-        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
-        encryptor.setPassword(bizConfig.getSecret());
-        encryptor.setAlgorithm(bizConfig.getAlgorithm());
-        encryptor.setSaltGenerator(new RandomSaltGenerator());
-        encryptor.setIvGenerator(new RandomIvGenerator());
-        return encryptor;
-    }
-}

+ 5 - 1
product-hunt/src/main/java/com/producthunt/server/core/config/ProductHuntConfig.java → product-hunt/src/main/java/com/producthunt/server/core/config/ProductHuntProperties.java

@@ -14,7 +14,7 @@ import lombok.Data;
 @Data
 @Component
 @ConfigurationProperties("product-hunt")
-public class ProductHuntConfig {
+public class ProductHuntProperties {
 
     /**
      * Product Hunt平台API地址
@@ -36,4 +36,8 @@ public class ProductHuntConfig {
      */
     private String clientSecret;
 
+    /**
+     * 榜单获取数量
+     */
+    private Integer rankNum;
 }

+ 27 - 15
product-hunt/src/main/java/com/producthunt/server/core/interceptor/AuthInterceptor.java

@@ -1,8 +1,10 @@
 package com.producthunt.server.core.interceptor;
 
 import com.producthunt.server.common.LoginException;
-import com.producthunt.server.core.config.BizConfig;
+import com.producthunt.server.core.config.BizProperties;
 import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
+import org.jasypt.iv.RandomIvGenerator;
+import org.jasypt.salt.RandomSaltGenerator;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
@@ -24,28 +26,38 @@ import lombok.extern.slf4j.Slf4j;
 public class AuthInterceptor implements HandlerInterceptor {
 
     @Autowired
-    private BizConfig bizConfig;
+    private BizProperties bizProperties;
 
-    @Autowired
+    /**
+     * 加密器
+     */
     private StandardPBEStringEncryptor encryptor;
 
+    @Autowired
+    public AuthInterceptor(BizProperties bizProperties) {
+        encryptor = new StandardPBEStringEncryptor();
+        encryptor.setPassword(bizProperties.getPassword());
+        encryptor.setAlgorithm(bizProperties.getAlgorithm());
+        encryptor.setSaltGenerator(new RandomSaltGenerator());
+        encryptor.setIvGenerator(new RandomIvGenerator());
+    }
+
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
             throws Exception {
 
-        // TODO 这里要修改
         // 判断请求头中是否有token
-//        String apiKeyStr = request.getHeader("Authorization");
-//        if (!StringUtils.hasText(apiKeyStr)) {
-//            throw LoginException.fail("登录校验异常,原因:没有token凭据");
-//        }
-//
-//        // TODO 这个加解密部分要加点变化的内容
-//        String apiKey = encryptor.decrypt(apiKeyStr);
-//        if (!bizConfig.getApiKey().equals(apiKey)) {
-//            log.warn("登录校验失败,apiKey不符,apiKey:{}", apiKey);
-//            throw LoginException.fail("登录校验失败,apiKey不符");
-//        }
+        String apiKeyStr = request.getHeader("Authorization");
+        if (!StringUtils.hasText(apiKeyStr)) {
+            throw LoginException.fail("登录校验异常,原因:没有访问凭据");
+        }
+
+        // 校验api key
+        String apiKey = encryptor.decrypt(apiKeyStr);
+        if (!bizProperties.getApiKey().equals(apiKey)) {
+            log.warn("登录校验失败,apiKey不符,apiKey:{}", apiKey);
+            throw LoginException.fail("登录校验失败,apiKey不符");
+        }
 
         return true;
     }

+ 17 - 350
product-hunt/src/main/java/com/producthunt/server/service/controller/ProductHuntController.java

@@ -1,23 +1,21 @@
 package com.producthunt.server.service.controller;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.databind.util.JSONPObject;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.producthunt.server.dto.PostNode;
+import java.util.List;
+
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.producthunt.server.dto.PostNode;
 import com.producthunt.server.service.manager.IProductHuntManager;
 
+import jakarta.validation.constraints.Pattern;
 import lombok.extern.slf4j.Slf4j;
 
-import java.util.List;
-
 /**
  * @author tyuio
  * @version 1.0.0
@@ -25,6 +23,7 @@ import java.util.List;
  * @date 2025/3/5 11:20
  */
 @Slf4j
+@Validated
 @RestController
 @RequestMapping("/producthunt")
 public class ProductHuntController {
@@ -32,346 +31,14 @@ public class ProductHuntController {
     @Autowired
     private IProductHuntManager productHuntManager;
 
-    private String content = "[\n" +
-            "        {\n" +
-            "            \"id\": 935408,\n" +
-            "            \"name\": \"Wispr Flow for Windows\",\n" +
-            "            \"tagline\": \"Stop typing, start speaking: 3x faster dictation on PC & Mac\",\n" +
-            "            \"description\": \"Tired of typing? Wispr Flow for Windows lets you speak naturally and see your words perfectly formatted—no extra edits, no typos. It’s the easiest way to write 3x faster across all your apps.\",\n" +
-            "            \"votesCount\": 621,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/VEQJCWAHVPXBLC?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/wispr-flow-for-windows?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 931928,\n" +
-            "            \"name\": \"No Cap\",\n" +
-            "            \"tagline\": \"World's first AI angel investor\",\n" +
-            "            \"description\": \"Time to come clean: I just invested $100k in a startup — and I'm an AI. Full announcement and video below \uD83D\uDC47\",\n" +
-            "            \"votesCount\": 458,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/RX3O54ISJJ42UG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/no-cap?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 941045,\n" +
-            "            \"name\": \"Cuckoo\",\n" +
-            "            \"tagline\": \"Real-time AI translator for global teams\",\n" +
-            "            \"description\": \"Cuckoo is a real-time AI translator for global sales, marketing, and support. Cuckoo helps companies like Snowflake and PagerDuty talk to their global customers in Zoom in-person meetings, even in the most technical discussions.\",\n" +
-            "            \"votesCount\": 384,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/BSLO27YPI7UOLG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/cuckoo-6?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 938270,\n" +
-            "            \"name\": \"Sherloq\",\n" +
-            "            \"tagline\": \"Create one place for all your SQL, directly on your editor\",\n" +
-            "            \"description\": \"Sherloq is a Collaborative SQL Repository. Using the AI-powered plugin, SQL users automatically save, organize, share, and document queries on-the-fly, without leaving the editor. Companies use Sherloq as the single source of truth for all their queries.\",\n" +
-            "            \"votesCount\": 358,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/M7N4WQZQIP4IOJ?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/sherloq-2?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 939950,\n" +
-            "            \"name\": \"Numeral\",\n" +
-            "            \"tagline\": \"Spend less than 5 minutes per month on sales tax compliance\",\n" +
-            "            \"description\": \"Numeral puts sales tax on autopilot for leading e-commerce and SaaS businesses, offering intelligent workflows for registration, filing, and remittance alongside white-glove customer support.\",\n" +
-            "            \"votesCount\": 339,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/EQNJ7UZ55RUAHG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/numeral?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 929040,\n" +
-            "            \"name\": \"OG PM Agent: Beta Release\",\n" +
-            "            \"tagline\": \"Your AI Product Manager\",\n" +
-            "            \"description\": \"PM Agent is your AI Product Manager that attends all your meetings, creates detailed summaries, and converts business discussions into Product Requirement Document including acceptance criteria for every sprint in real time keeping all stakeholders aligned.\",\n" +
-            "            \"votesCount\": 247,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/HYURCPOABM6B7B?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/og-pm-agent-beta-release?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940539,\n" +
-            "            \"name\": \"Superwall\",\n" +
-            "            \"tagline\": \"Build & test mobile app paywalls without shipping updates\",\n" +
-            "            \"description\": \"Superwall is the ultimate paywall solution for mobile apps. Build and test unlimited paywall designs, pricing, and A/B experiments— all without shipping app updates. With built in analytics, reduce your time to experiment by 90% and grow revenue by 20-30%.\",\n" +
-            "            \"votesCount\": 227,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/JEBXUG7IVV3WMG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/superwall?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940968,\n" +
-            "            \"name\": \"OpenAI Responses API and Agents SDK\",\n" +
-            "            \"tagline\": \"New tools for building agents and tools\",\n" +
-            "            \"description\": \"A new set of APIs and tools specifically designed to simplify the development of agentic applications.\",\n" +
-            "            \"votesCount\": 174,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/7ZEJQUN3IQPUZA?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/openai-responses-api-and-agents-sdk?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940914,\n" +
-            "            \"name\": \"AI Renamer\",\n" +
-            "            \"tagline\": \"Rename your files with AI\",\n" +
-            "            \"description\": \"Automatically rename your files based on their content using AI. Perfect for organizing images and documents with meaningful names.\",\n" +
-            "            \"votesCount\": 167,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/DND2CRR6HKS53X?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/ai-renamer-2?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940701,\n" +
-            "            \"name\": \"Omlet for VS Code\",\n" +
-            "            \"tagline\": \"Get React component usage insights in VS Code\",\n" +
-            "            \"description\": \"Omlet is a component analytics tool for React. You can now use Omlet directly in VS Code and analyze how and where your React components (and their props) are used as you're coding — helping you confidently update, clean up and maintain your component library.\",\n" +
-            "            \"votesCount\": 163,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/DOVGY6PD4WK77S?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/omlet-for-vs-code?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940824,\n" +
-            "            \"name\": \"FirstSeed Tasks\",\n" +
-            "            \"tagline\": \"Task manager focused on task execution & completion\",\n" +
-            "            \"description\": \"Introducing FirstSeed Tasks 4 – a task manager that is focused on task execution and completion. With a built-in timer and Pomodoro functionality, it keeps you focused, enhances efficiency, and maximizes productivity. Available on iPhone, iPad, and Mac.\",\n" +
-            "            \"votesCount\": 153,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/A6Q5G6U66ELHPP?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/firstseed-tasks?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 939735,\n" +
-            "            \"name\": \"Sahha Archetypes\",\n" +
-            "            \"tagline\": \"Segment users based on their health, lifestyle, & behavior!\",\n" +
-            "            \"description\": \"Categorize users based on long-term health, lifestyle, and behavioral trends, enabling hyper-personalized engagement and retention strategies. Archetypes are intuitive, easy-to-use labels that capture a person’s persona, health and lifestyle\",\n" +
-            "            \"votesCount\": 153,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/KFNWL2Q64CTORR?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/sahha-archetypes?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 939053,\n" +
-            "            \"name\": \"Search Copilot\",\n" +
-            "            \"tagline\": \"Work AI across platforms\",\n" +
-            "            \"description\": \"Search Copilot is the AI-powered search engine for your work. Instantly find insights from meetings, emails, chats, docs, and CRMs—all in one place. No setup hassle, privacy-first, and free. Stop searching, start finding. \uD83D\uDE80\",\n" +
-            "            \"votesCount\": 141,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/7PCPCKCIRCKM3Z?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/search-copilot?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940919,\n" +
-            "            \"name\": \"PlanetScale Metal\",\n" +
-            "            \"tagline\": \"The fastest way to run databases in AWS or GCP\",\n" +
-            "            \"description\": \"PlanetScale Metal is the fastest way to run databases in AWS or GCP. With blazing fast NVMe drives, you can unlock unlimited IOPS, ultra low latencies, and the highest throughput for your workloads.\",\n" +
-            "            \"votesCount\": 130,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/A3UUCYI5Q7MH76?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/planetscale-metal?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940792,\n" +
-            "            \"name\": \"DJI Dock 3\",\n" +
-            "            \"tagline\": \"Rise to Any Challenge\",\n" +
-            "            \"description\": \"Introducing the DJI Dock 3, DJI's First Dock Adaptable for Vehicle Mounting. The DJI Dock 3 empowers 24/7 remote operations and effortlessly adapts to various environments.\",\n" +
-            "            \"votesCount\": 125,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/GHI7E7TT5CCHW5?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/dji-dock-3?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940946,\n" +
-            "            \"name\": \"Supadex for iOS & Android\",\n" +
-            "            \"tagline\": \"Supabase data in your pocket\",\n" +
-            "            \"description\": \"The ultimate mobile dashboard for Supabase. Manage databases, track metrics, and monitor projects seamlessly, anytime, anywhere.\",\n" +
-            "            \"votesCount\": 118,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/SUQDWEYZSJKTZF?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/supadex-for-ios-android?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940774,\n" +
-            "            \"name\": \"Pod\",\n" +
-            "            \"tagline\": \"A desktop iPod that plays local music and radio\",\n" +
-            "            \"description\": \"Pod is a desktop music player which feels and looks like an iPod. Enjoy seamless navigation, haptic feedback, and stylish design, all without extra hardware. Listen to local music files, radio and soon Spotify.\",\n" +
-            "            \"votesCount\": 115,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/EYQMXQVYVKJ7YJ?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/pod-6?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940617,\n" +
-            "            \"name\": \"W3ARE v1 beta\",\n" +
-            "            \"tagline\": \"Accept payments in any currency or crypto\",\n" +
-            "            \"description\": \"Simplify international transactions with w3are. Create payment links that support multiple payment methods, currencies, and cryptocurrencies. Secure, compliant, and easy to use. Start accepting payments worldwide today!\",\n" +
-            "            \"votesCount\": 108,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/OGNMM5QABG7HLJ?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/w3are-v1-beta?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940536,\n" +
-            "            \"name\": \"FocusNudge\",\n" +
-            "            \"tagline\": \"Status bar app to nudge you back into focus!\",\n" +
-            "            \"description\": \"Stay on track in a distraction-filled world! FocusNudge gently reminds you of your main focus whenever you switch to distracting apps. This tiny menu bar app delivers perfectly timed nudges that keep you aligned with what matters. Customizable, unobtrusive.\",\n" +
-            "            \"votesCount\": 108,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/HW627AGBESAL3V?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/focusnudge?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940392,\n" +
-            "            \"name\": \"Pocket Casts\",\n" +
-            "            \"tagline\": \"No Paywalls. No Walled Gardens. Just Podcasts.\",\n" +
-            "            \"description\": \"Pocket Casts is a powerful, beautifully designed podcast player that lets you stream your podcasts from any device. With intuitive playback controls and a clean interface, it’s the best way to enjoy podcasts—now free for all.\",\n" +
-            "            \"votesCount\": 106,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": null,\n" +
-            "            \"website\": \"https://www.producthunt.com/r/IIDXSG7EVDDS64?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/pocket-casts-8?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 935408,\n" +
-            "            \"name\": \"Wispr Flow for Windows\",\n" +
-            "            \"tagline\": \"Stop typing, start speaking: 3x faster dictation on PC & Mac\",\n" +
-            "            \"description\": \"Tired of typing? Wispr Flow for Windows lets you speak naturally and see your words perfectly formatted—no extra edits, no typos. It’s the easiest way to write 3x faster across all your apps.\",\n" +
-            "            \"votesCount\": 621,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/VEQJCWAHVPXBLC?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/wispr-flow-for-windows?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 931928,\n" +
-            "            \"name\": \"No Cap\",\n" +
-            "            \"tagline\": \"World's first AI angel investor\",\n" +
-            "            \"description\": \"Time to come clean: I just invested $100k in a startup — and I'm an AI. Full announcement and video below \uD83D\uDC47\",\n" +
-            "            \"votesCount\": 458,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/RX3O54ISJJ42UG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/no-cap?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 941045,\n" +
-            "            \"name\": \"Cuckoo\",\n" +
-            "            \"tagline\": \"Real-time AI translator for global teams\",\n" +
-            "            \"description\": \"Cuckoo is a real-time AI translator for global sales, marketing, and support. Cuckoo helps companies like Snowflake and PagerDuty talk to their global customers in Zoom in-person meetings, even in the most technical discussions.\",\n" +
-            "            \"votesCount\": 384,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/BSLO27YPI7UOLG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/cuckoo-6?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 938270,\n" +
-            "            \"name\": \"Sherloq\",\n" +
-            "            \"tagline\": \"Create one place for all your SQL, directly on your editor\",\n" +
-            "            \"description\": \"Sherloq is a Collaborative SQL Repository. Using the AI-powered plugin, SQL users automatically save, organize, share, and document queries on-the-fly, without leaving the editor. Companies use Sherloq as the single source of truth for all their queries.\",\n" +
-            "            \"votesCount\": 358,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/M7N4WQZQIP4IOJ?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/sherloq-2?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 939950,\n" +
-            "            \"name\": \"Numeral\",\n" +
-            "            \"tagline\": \"Spend less than 5 minutes per month on sales tax compliance\",\n" +
-            "            \"description\": \"Numeral puts sales tax on autopilot for leading e-commerce and SaaS businesses, offering intelligent workflows for registration, filing, and remittance alongside white-glove customer support.\",\n" +
-            "            \"votesCount\": 339,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/EQNJ7UZ55RUAHG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/numeral?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 929040,\n" +
-            "            \"name\": \"OG PM Agent: Beta Release\",\n" +
-            "            \"tagline\": \"Your AI Product Manager\",\n" +
-            "            \"description\": \"PM Agent is your AI Product Manager that attends all your meetings, creates detailed summaries, and converts business discussions into Product Requirement Document including acceptance criteria for every sprint in real time keeping all stakeholders aligned.\",\n" +
-            "            \"votesCount\": 247,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/HYURCPOABM6B7B?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/og-pm-agent-beta-release?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940539,\n" +
-            "            \"name\": \"Superwall\",\n" +
-            "            \"tagline\": \"Build & test mobile app paywalls without shipping updates\",\n" +
-            "            \"description\": \"Superwall is the ultimate paywall solution for mobile apps. Build and test unlimited paywall designs, pricing, and A/B experiments— all without shipping app updates. With built in analytics, reduce your time to experiment by 90% and grow revenue by 20-30%.\",\n" +
-            "            \"votesCount\": 227,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/JEBXUG7IVV3WMG?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/superwall?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940968,\n" +
-            "            \"name\": \"OpenAI Responses API and Agents SDK\",\n" +
-            "            \"tagline\": \"New tools for building agents and tools\",\n" +
-            "            \"description\": \"A new set of APIs and tools specifically designed to simplify the development of agentic applications.\",\n" +
-            "            \"votesCount\": 174,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/7ZEJQUN3IQPUZA?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/openai-responses-api-and-agents-sdk?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940914,\n" +
-            "            \"name\": \"AI Renamer\",\n" +
-            "            \"tagline\": \"Rename your files with AI\",\n" +
-            "            \"description\": \"Automatically rename your files based on their content using AI. Perfect for organizing images and documents with meaningful names.\",\n" +
-            "            \"votesCount\": 167,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/DND2CRR6HKS53X?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/ai-renamer-2?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        },\n" +
-            "        {\n" +
-            "            \"id\": 940701,\n" +
-            "            \"name\": \"Omlet for VS Code\",\n" +
-            "            \"tagline\": \"Get React component usage insights in VS Code\",\n" +
-            "            \"description\": \"Omlet is a component analytics tool for React. You can now use Omlet directly in VS Code and analyze how and where your React components (and their props) are used as you're coding — helping you confidently update, clean up and maintain your component library.\",\n" +
-            "            \"votesCount\": 163,\n" +
-            "            \"createdAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"featuredAt\": \"2025-03-12T07:01:00\",\n" +
-            "            \"website\": \"https://www.producthunt.com/r/DOVGY6PD4WK77S?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\",\n" +
-            "            \"url\": \"https://www.producthunt.com/posts/omlet-for-vs-code?utm_campaign=producthunt-api&utm_medium=api-v2&utm_source=Application%3A+dataeasy+%28ID%3A+170125%29\"\n" +
-            "        }\n" +
-            "    ]";
-
-    @GetMapping("/getTop30Posts")
-    public List<PostNode> getTop30Posts() throws JsonProcessingException {
-//        return productHuntManager.getTop30Posts();
-        ObjectMapper objectMapper = new ObjectMapper();
-        objectMapper.registerModule(new JavaTimeModule());
-        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
-        List<PostNode> stringList = objectMapper.readValue(content, new TypeReference<List<PostNode>>() {});
-        return stringList;
+    /**
+     * 获取热榜列表
+     * @param rankDate 榜单日期
+     * @return
+     * @throws JsonProcessingException
+     */
+    @GetMapping("/getPosts")
+    public List<PostNode> getPosts(@RequestParam @Validated @Pattern(regexp = "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", message = "字符串格式不正确,格式:yyyy-MM-dd") String rankDate) throws JsonProcessingException {
+        return productHuntManager.getPosts(rankDate);
     }
 }

+ 3 - 2
product-hunt/src/main/java/com/producthunt/server/service/manager/IProductHuntManager.java

@@ -13,8 +13,9 @@ import java.util.List;
 public interface IProductHuntManager {
 
     /**
-     * 查询Product Hunt Top30 的帖子(当天)
+     * 查询Product Hunt 热榜帖子
+     * @param rankDate 榜单日期
      * @return
      */
-    List<PostNode> getTop30Posts();
+    List<PostNode> getPosts(String rankDate);
 }

+ 26 - 27
product-hunt/src/main/java/com/producthunt/server/service/manager/impl/ProductHuntManagerImpl.java

@@ -1,24 +1,25 @@
 package com.producthunt.server.service.manager.impl;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.graphql.client.GraphQlClient;
+import org.springframework.graphql.client.HttpSyncGraphQlClient;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import com.producthunt.server.core.config.ProductHuntProperties;
 import com.producthunt.server.dto.PostNode;
 import com.producthunt.server.dto.PostResponse;
-import com.producthunt.server.core.config.ProductHuntConfig;
 import com.producthunt.server.feign.ProductHuntFeign;
 import com.producthunt.server.feign.dto.OauthRequest;
 import com.producthunt.server.feign.dto.OauthResponse;
 import com.producthunt.server.service.manager.IProductHuntManager;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.graphql.client.GraphQlClient;
-import org.springframework.graphql.client.HttpSyncGraphQlClient;
-import org.springframework.stereotype.Component;
-import org.springframework.util.CollectionUtils;
 
-import java.text.SimpleDateFormat;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
+import lombok.extern.slf4j.Slf4j;
 
 /**
  * @author tyuio
@@ -30,16 +31,11 @@ import java.util.List;
 @Component
 public class ProductHuntManagerImpl implements IProductHuntManager {
 
-    /**
-     * 日期格式化器,格式:yyyyMMdd
-     */
-    public static final DateTimeFormatter YYYY_MM_DD_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-
     @Autowired
     private ProductHuntFeign productHuntFeign;
 
     @Autowired
-    private ProductHuntConfig productHuntConfig;
+    private ProductHuntProperties productHuntProperties;
 
     /**
      * Product Hunt 查询模板(按投票数排序,限定时间范围)
@@ -68,30 +64,32 @@ public class ProductHuntManagerImpl implements IProductHuntManager {
 
     private String getAccessToken() {
         OauthRequest oauthRequest = new OauthRequest();
-        oauthRequest.setClientId(productHuntConfig.getClientId());
-        oauthRequest.setClientSecret(productHuntConfig.getClientSecret());
+        oauthRequest.setClientId(productHuntProperties.getClientId());
+        oauthRequest.setClientSecret(productHuntProperties.getClientSecret());
         OauthResponse oauthToken = productHuntFeign.getOauthToken(oauthRequest);
         return oauthToken.getAccessToken();
     }
 
     @Override
-    public List<PostNode> getTop30Posts() {
-        // 获取查询的日期范围
-        String dateStr = YYYY_MM_DD_FORMATTER.format(LocalDate.now().minusDays(1));
+    public List<PostNode> getPosts(String rankDate) {
+        if (!StringUtils.hasText(rankDate)) {
+            return List.of();
+        }
 
         // 构造GraphQl客户端
         String accessToken = getAccessToken();
         String bearerToken = String.format("Bearer %s", accessToken);
-        GraphQlClient graphQlClient = HttpSyncGraphQlClient.builder().url(productHuntConfig.getGraphqlUrl())
+        GraphQlClient graphQlClient = HttpSyncGraphQlClient.builder().url(productHuntProperties.getGraphqlUrl())
                 .header("Authorization", bearerToken).build();
 
         // 查询
         List<PostNode> posts = new ArrayList<>();
         boolean hasNextPage = true;
         String endCursor = "";
+        int rankNum = Optional.ofNullable(productHuntProperties.getRankNum()).orElse(0);
         while (hasNextPage) {
             // 设置查询模板并发起查询,好像无法直接一次性查询指定数量,只能分页查询并且要自己控制所需数量
-            String queryDocument = String.format(queryTemplate, dateStr, dateStr, endCursor);
+            String queryDocument = String.format(queryTemplate, rankDate, rankDate, endCursor);
             PostResponse postResponse =
                     graphQlClient.document(queryDocument).retrieveSync("posts").toEntity(PostResponse.class);
             // 把获取到的帖子信息放入容器
@@ -99,7 +97,7 @@ public class ProductHuntManagerImpl implements IProductHuntManager {
                 posts.addAll(postResponse.getNodes());
             }
             // 帖子数量大于30则退出
-            if (posts.size() > 30) {
+            if (posts.size() > rankNum) {
                 break;
             }
             // 没有下一页了则跳出循环
@@ -111,6 +109,7 @@ public class ProductHuntManagerImpl implements IProductHuntManager {
         if (CollectionUtils.isEmpty(posts)) {
             return List.of();
         }
-        return posts.subList(0, 30);
+
+        return posts.subList(0, rankNum <= posts.size() ? rankNum : posts.size());
     }
 }

+ 2 - 2
product-hunt/src/main/resources/application-dev.yaml

@@ -5,7 +5,7 @@ product-hunt:
 
 # 系统配置
 biz:
-  # 加密密
-  secret: ENC(2wscdOD7kzByW3VNDQkOZq2BdoM6PS7peINZRwJjEwVubsFN437X3g==)
+  # 加密密
+  password: ENC(2wscdOD7kzByW3VNDQkOZq2BdoM6PS7peINZRwJjEwVubsFN437X3g==)
   # 系统自访问密钥
   api-key: ENC(S3TSPw9QuJwvZScgrOv1ORm089BkXy6EJJNeFYwRVxqZnBb176rSXA==)

+ 2 - 2
product-hunt/src/main/resources/application-prod.yaml

@@ -5,7 +5,7 @@ product-hunt:
 
 # 系统配置
 biz:
-  # 加密密
-  secret: ENC(V0XvB+A9qK6Xmd1q7M9Bjls8aW4QH3m91GMcr+zpiAIhXFiLALCn+w==)
+  # 加密密
+  password: ENC(V0XvB+A9qK6Xmd1q7M9Bjls8aW4QH3m91GMcr+zpiAIhXFiLALCn+w==)
   # 系统自访问密钥
   api-key: ENC(z2rDV5xcbhkO3JuGB83J+0GmLREenc7ucsn0s5POe2NtMtblIgdOfQ==)

+ 2 - 0
product-hunt/src/main/resources/application.yaml

@@ -17,6 +17,8 @@ product-hunt:
   base-url: https://api.producthunt.com
   # GraphQL 接口地址
   graphql-url: https://api.producthunt.com/v2/api/graphql
+  # 榜单获取数量
+  rank-num: 30
 
 # 系统配置
 biz: